snow-flow 10.0.1-dev.424 → 10.0.1-dev.427

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.
@@ -1,17 +1,3 @@
1
- /**
2
- * snow_manage_flow - Complete Flow Designer lifecycle management
3
- *
4
- * Create, list, get, update, activate, deactivate, delete and publish
5
- * Flow Designer flows and subflows.
6
- *
7
- * For create/create_subflow: uses a Scheduled Job (sysauto_script) that
8
- * runs GlideRecord server-side, ensuring sys_hub_flow_version records are
9
- * created, flow_definition is set, and sn_fd engine registration is
10
- * attempted. Falls back to Table API if the scheduled job fails.
11
- *
12
- * All other actions (list, get, update, activate, etc.) use the Table API.
13
- */
14
-
15
1
  import { MCPToolDefinition, ServiceNowContext, ToolResult } from '../../shared/types.js';
16
2
  import { getAuthenticatedClient } from '../../shared/auth.js';
17
3
  import { createSuccessResult, createErrorResult, SnowFlowError, ErrorType } from '../../shared/error-handler.js';
@@ -31,1209 +17,404 @@ function isSysId(value: string): boolean {
31
17
  return /^[a-f0-9]{32}$/.test(value);
32
18
  }
33
19
 
34
- /** Escape a string for safe embedding in ES5 single-quoted script */
35
- function escForScript(s: string): string {
36
- return s.replace(/\\/g, '\\\\').replace(/'/g, "\\'").replace(/\n/g, '\\n').replace(/\r/g, '');
20
+ // ── GraphQL Flow Designer helpers ─────────────────────────────────────
21
+
22
+ function jsToGraphQL(val: any): string {
23
+ if (val === null || val === undefined) return 'null';
24
+ if (typeof val === 'string') return JSON.stringify(val);
25
+ if (typeof val === 'number' || typeof val === 'bigint') return String(val);
26
+ if (typeof val === 'boolean') return val ? 'true' : 'false';
27
+ if (Array.isArray(val)) return '[' + val.map(jsToGraphQL).join(', ') + ']';
28
+ if (typeof val === 'object') {
29
+ return '{' + Object.entries(val).map(([k, v]) => k + ': ' + jsToGraphQL(v)).join(', ') + '}';
30
+ }
31
+ return String(val);
37
32
  }
38
33
 
39
- /**
40
- * Create a flow via a ServiceNow Scheduled Job (sysauto_script).
41
- *
42
- * This executes server-side GlideRecord code that CAN set computed fields
43
- * like `latest_version`, which the Table API silently ignores.
44
- *
45
- * Strategy:
46
- * 1. Create a sys_properties record to receive the result
47
- * 2. Create a sysauto_script record (run_type=once, run_start=now)
48
- * 3. Poll the property every 2s for up to 30s
49
- * 4. Clean up both records
50
- * 5. Return the result (flow sys_id, version sys_id, tier used)
51
- */
52
- async function createFlowViaScheduledJob(
34
+ function generateUUID(): string {
35
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, (c) => {
36
+ const r = (Math.random() * 16) | 0;
37
+ return (c === 'x' ? r : (r & 0x3) | 0x8).toString(16);
38
+ });
39
+ }
40
+
41
+ async function executeFlowPatchMutation(
53
42
  client: any,
54
- params: {
55
- name: string;
56
- description: string;
57
- internalName: string;
58
- isSubflow: boolean;
59
- category: string;
60
- runAs: string;
61
- shouldActivate: boolean;
62
- triggerType?: string;
63
- triggerTable?: string;
64
- triggerCondition?: string;
65
- activities?: Array<{ name: string; type?: string; inputs?: any }>;
66
- inputs?: Array<{ name: string; label?: string; type?: string; mandatory?: boolean; default_value?: string }>;
67
- outputs?: Array<{ name: string; label?: string; type?: string }>;
68
- flowDefinition?: any;
43
+ flowPatch: any,
44
+ responseFields: string
45
+ ): Promise<any> {
46
+ const mutation = 'mutation { global { snFlowDesigner { flow(flowPatch: ' +
47
+ jsToGraphQL(flowPatch) + ') { id ' + responseFields + ' __typename } __typename } __typename } }';
48
+ const resp = await client.post('/api/now/graphql', { variables: {}, query: mutation });
49
+ const errors = resp.data?.errors;
50
+ if (errors && errors.length > 0) {
51
+ throw new Error('GraphQL error: ' + JSON.stringify(errors[0].message || errors[0]));
69
52
  }
70
- ): Promise<{
71
- success: boolean;
72
- flowSysId?: string;
73
- versionSysId?: string;
74
- tierUsed?: string;
75
- latestVersionSet?: boolean;
76
- latestVersionValue?: string;
77
- steps?: any;
78
- error?: string;
79
- }> {
80
- var resultPropName = 'snow_flow.factory_result.' + Date.now();
81
- var flowDefStr = params.flowDefinition ? JSON.stringify(params.flowDefinition) : '';
82
-
83
- // Build the server-side ES5 script
84
- var script = [
85
- "(function() {",
86
- " var PROP = '" + escForScript(resultPropName) + "';",
87
- " var r = { success: false, steps: {}, tier_used: null };",
88
- " try {",
89
- " var flowName = '" + escForScript(params.name) + "';",
90
- " var flowDesc = '" + escForScript(params.description) + "';",
91
- " var intName = '" + escForScript(params.internalName) + "';",
92
- " var isSubflow = " + (params.isSubflow ? "true" : "false") + ";",
93
- " var flowCat = '" + escForScript(params.category) + "';",
94
- " var runAs = '" + escForScript(params.runAs) + "';",
95
- " var activate = " + (params.shouldActivate ? "true" : "false") + ";",
96
- " var trigType = '" + escForScript(params.triggerType || 'manual') + "';",
97
- " var trigTable = '" + escForScript(params.triggerTable || '') + "';",
98
- " var trigCondition = '" + escForScript(params.triggerCondition || '') + "';",
99
- " var flowDefStr = '" + escForScript(flowDefStr) + "';",
100
- " var activitiesJson = '" + escForScript(JSON.stringify(params.activities || [])) + "';",
101
- " var inputsJson = '" + escForScript(JSON.stringify(params.inputs || [])) + "';",
102
- " var outputsJson = '" + escForScript(JSON.stringify(params.outputs || [])) + "';",
103
- " var activities = []; try { activities = JSON.parse(activitiesJson); } catch(e) {}",
104
- " var inputs = []; try { inputs = JSON.parse(inputsJson); } catch(e) {}",
105
- " var outputs = []; try { outputs = JSON.parse(outputsJson); } catch(e) {}",
106
- " var flowSysId = null;",
107
- " var verSysId = null;",
108
- "",
109
- // ── TIER 1: sn_fd.FlowDesigner API ──
110
- " try {",
111
- " if (typeof sn_fd !== 'undefined' && sn_fd.FlowDesigner && typeof sn_fd.FlowDesigner.createFlow === 'function') {",
112
- " var fdR = sn_fd.FlowDesigner.createFlow({ name: flowName, description: flowDesc, type: isSubflow ? 'subflow' : 'flow', category: flowCat, run_as: runAs });",
113
- " if (fdR) {",
114
- " flowSysId = (typeof fdR === 'object' ? (fdR.sys_id || (fdR.getValue ? fdR.getValue('sys_id') : null)) : fdR) + '';",
115
- " r.tier_used = 'sn_fd_api';",
116
- " r.success = true;",
117
- " r.steps.tier1 = { success: true };",
118
- " if (activate && typeof sn_fd.FlowDesigner.publishFlow === 'function') {",
119
- " try { sn_fd.FlowDesigner.publishFlow(flowSysId); r.steps.publish = { success: true }; }",
120
- " catch(pe) { r.steps.publish = { success: false, error: pe + '' }; }",
121
- " }",
122
- " }",
123
- " }",
124
- " } catch(t1e) { r.steps.tier1 = { success: false, error: t1e.getMessage ? t1e.getMessage() : t1e + '' }; }",
125
- "",
126
- // ── TIER 2: GlideRecord ──
127
- // Reference flow analysis:
128
- // - latest_snapshot points to sys_hub_flow_snapshot (NOT version, NOT JSON)
129
- // - flow_definition on flow record = null
130
- // - latest_version = null (normal)
131
- // Pipeline: flow → version → snapshot → set latest_snapshot on flow
132
- " if (!flowSysId) {",
133
- " try {",
134
- " var f = new GlideRecord('sys_hub_flow');",
135
- " f.initialize();",
136
- " f.setValue('name', flowName); f.setValue('description', flowDesc);",
137
- " f.setValue('internal_name', intName); f.setValue('category', flowCat);",
138
- " f.setValue('run_as', runAs); f.setValue('active', false);",
139
- " f.setValue('status', 'draft'); f.setValue('validated', true);",
140
- " f.setValue('type', isSubflow ? 'subflow' : 'flow');",
141
- " flowSysId = f.insert();",
142
- " r.steps.flow_insert = { success: !!flowSysId, sys_id: flowSysId + '' };",
143
- " if (flowSysId) {",
144
- // Version record
145
- " var v = new GlideRecord('sys_hub_flow_version');",
146
- " v.initialize();",
147
- " v.setValue('flow', flowSysId); v.setValue('name', '1.0');",
148
- " v.setValue('version', '1.0'); v.setValue('state', 'draft');",
149
- " v.setValue('active', true); v.setValue('compile_state', 'draft');",
150
- " v.setValue('is_current', true);",
151
- " if (flowDefStr) { v.setValue('flow_definition', flowDefStr); }",
152
- " verSysId = v.insert();",
153
- " r.steps.version_insert = { success: !!verSysId, sys_id: verSysId + '' };",
154
- "",
155
- // No snapshot/latest_snapshot — let the engine create these via Business Rule post-compile
156
- " r.tier_used = 'gliderecord_scheduled'; r.success = true;",
157
- " }",
158
- " } catch(t2e) { r.steps.tier2 = { success: false, error: t2e.getMessage ? t2e.getMessage() : t2e + '' }; }",
159
- " }",
160
- "",
161
- // ── Trigger, action, variable creation (runs for any tier) ──
162
- " if (flowSysId) {",
163
- // Trigger — precise search with prefix matching + fallback without action_type
164
- " if (!isSubflow && trigType !== 'manual') {",
165
- " try {",
166
- " var trigDefId = null;",
167
- " var trigDefName = '';",
168
- " var trigSearchLog = [];",
169
- "",
170
- // Search 1: exact prefix 'sn_fd.trigger.' (standard Flow Designer naming)
171
- " var exactNames = {",
172
- " 'record_created': ['sn_fd.trigger.record_created', 'global.sn_fd.trigger.record_created'],",
173
- " 'record_updated': ['sn_fd.trigger.record_updated', 'global.sn_fd.trigger.record_updated'],",
174
- " 'scheduled': ['sn_fd.trigger.scheduled', 'global.sn_fd.trigger.scheduled']",
175
- " };",
176
- " var exactCandidates = exactNames[trigType] || [];",
177
- " for (var ec = 0; ec < exactCandidates.length; ec++) {",
178
- " var tgE = new GlideRecord('sys_hub_action_type_definition');",
179
- " tgE.addQuery('internal_name', exactCandidates[ec]);",
180
- " tgE.setLimit(1); tgE.query();",
181
- " if (tgE.next()) {",
182
- " trigDefId = tgE.getUniqueValue(); trigDefName = tgE.getValue('name');",
183
- " trigSearchLog.push('exact:' + exactCandidates[ec] + ':found');",
184
- " break;",
185
- " }",
186
- " trigSearchLog.push('exact:' + exactCandidates[ec] + ':not_found');",
187
- " }",
188
- "",
189
- // Search 2: prefix STARTSWITH 'sn_fd.trigger' (catches variations)
190
- " if (!trigDefId) {",
191
- " var tgP = new GlideRecord('sys_hub_action_type_definition');",
192
- " tgP.addQuery('internal_name', 'STARTSWITH', 'sn_fd.trigger');",
193
- " tgP.setLimit(10); tgP.query();",
194
- " var prefixMatches = [];",
195
- " while (tgP.next()) {",
196
- " prefixMatches.push(tgP.getValue('internal_name'));",
197
- " if (!trigDefId && (tgP.getValue('internal_name') + '').indexOf(trigType.replace('record_', '')) > -1) {",
198
- " trigDefId = tgP.getUniqueValue(); trigDefName = tgP.getValue('name');",
199
- " }",
200
- " }",
201
- " trigSearchLog.push('prefix_sn_fd.trigger:[' + prefixMatches.join(',') + ']');",
202
- " }",
203
- "",
204
- // Search 3 removed — sys_hub_trigger_definition returned wrong defs (Proactive Analytics, DevOps)
205
- " if (!trigDefId) { trigSearchLog.push('no_exact_trigger_def_found'); }",
206
- "",
207
- // Create trigger instance — only set action_type if exact sn_fd.trigger.* match found
208
- " var trigInst = new GlideRecord('sys_hub_trigger_instance');",
209
- " trigInst.initialize();",
210
- " trigInst.setValue('flow', flowSysId);",
211
- " trigInst.setValue('name', trigType);",
212
- " trigInst.setValue('order', 0);",
213
- " trigInst.setValue('active', true);",
214
- " if (trigDefId) trigInst.setValue('action_type', trigDefId);",
215
- " if (trigTable) trigInst.setValue('table', trigTable);",
216
- " if (trigCondition) trigInst.setValue('condition', trigCondition);",
217
- " var trigId = trigInst.insert();",
218
- " r.steps.trigger = {",
219
- " success: !!trigId,",
220
- " sys_id: trigId + '',",
221
- " def_found: !!trigDefId,",
222
- " def_name: trigDefName || 'none (created without action_type)',",
223
- " search: trigSearchLog",
224
- " };",
225
- " } catch(te) { r.steps.trigger = { success: false, error: te.getMessage ? te.getMessage() : te + '' }; }",
226
- " }",
227
- // Actions
228
- " var actionsCreated = 0;",
229
- " for (var ai = 0; ai < activities.length; ai++) {",
230
- " try {",
231
- " var act = activities[ai];",
232
- " var actDef = new GlideRecord('sys_hub_action_type_definition');",
233
- " actDef.addQuery('internal_name', 'CONTAINS', act.type || 'script');",
234
- " actDef.addOrCondition('name', 'CONTAINS', act.type || 'script'); actDef.query();",
235
- " var actInst = new GlideRecord('sys_hub_action_instance');",
236
- " actInst.initialize();",
237
- " actInst.setValue('flow', flowSysId); actInst.setValue('name', act.name || 'Action ' + (ai + 1));",
238
- " actInst.setValue('order', (ai + 1) * 100); actInst.setValue('active', true);",
239
- " if (actDef.next()) actInst.setValue('action_type', actDef.getUniqueValue());",
240
- " if (actInst.insert()) actionsCreated++;",
241
- " } catch(ae) {}",
242
- " }",
243
- " r.steps.actions = { success: true, created: actionsCreated, requested: activities.length };",
244
- // Variables (subflows)
245
- " var varsCreated = 0;",
246
- " if (isSubflow) {",
247
- " for (var vi = 0; vi < inputs.length; vi++) {",
248
- " try { var inp = inputs[vi]; var fv = new GlideRecord('sys_hub_flow_variable'); fv.initialize();",
249
- " fv.setValue('flow', flowSysId); fv.setValue('name', inp.name); fv.setValue('label', inp.label || inp.name);",
250
- " fv.setValue('type', inp.type || 'string'); fv.setValue('mandatory', inp.mandatory || false);",
251
- " fv.setValue('default_value', inp.default_value || ''); fv.setValue('variable_type', 'input');",
252
- " if (fv.insert()) varsCreated++;",
253
- " } catch(ve) {}",
254
- " }",
255
- " for (var vo = 0; vo < outputs.length; vo++) {",
256
- " try { var ot = outputs[vo]; var ov = new GlideRecord('sys_hub_flow_variable'); ov.initialize();",
257
- " ov.setValue('flow', flowSysId); ov.setValue('name', ot.name); ov.setValue('label', ot.label || ot.name);",
258
- " ov.setValue('type', ot.type || 'string'); ov.setValue('variable_type', 'output');",
259
- " if (ov.insert()) varsCreated++;",
260
- " } catch(ve) {}",
261
- " }",
262
- " }",
263
- " r.steps.variables = { success: true, created: varsCreated };",
264
- // Engine compilation delegated to Business Rule (runs in Flow Designer context)
265
- " r.steps.engine = 'deferred_to_business_rule';",
266
- " }",
267
- "",
268
- " r.flow_sys_id = flowSysId ? flowSysId + '' : null;",
269
- " r.version_sys_id = verSysId ? verSysId + '' : null;",
270
- // ── Final check: read back our flow's latest_snapshot to confirm ──
271
- " if (flowSysId) {",
272
- " var cf = new GlideRecord('sys_hub_flow');",
273
- " if (cf.get(flowSysId)) {",
274
- " r.latest_version_value = cf.getValue('latest_version') + '';",
275
- " r.our_latest_snapshot = cf.getValue('latest_snapshot') + '';",
276
- " r.our_latest_snapshot_length = (cf.getValue('latest_snapshot') + '').length;",
277
- " }",
278
- " }",
279
- " } catch(e) { r.success = false; r.error = e.getMessage ? e.getMessage() : e + ''; }",
280
- " gs.setProperty(PROP, JSON.stringify(r));",
281
- "})();"
282
- ].join('\n');
53
+ return resp.data?.data?.global?.snFlowDesigner?.flow || resp.data;
54
+ }
283
55
 
284
- try {
285
- // 1. Create result property
286
- await client.post('/api/now/table/sys_properties', {
287
- name: resultPropName,
288
- value: 'pending',
289
- type: 'string',
290
- description: 'Snow-Flow temp result (auto-cleanup)'
291
- });
292
-
293
- // 2. Create scheduled job (run immediately)
294
- var runStart = new Date().toISOString().replace('T', ' ').substring(0, 19);
295
- var jobResp = await client.post('/api/now/table/sysauto_script', {
296
- name: 'Snow-Flow Flow Factory (auto-cleanup)',
297
- script: script,
298
- run_type: 'once',
299
- run_start: runStart,
300
- active: true
301
- });
302
- var jobSysId = jobResp.data.result?.sys_id;
303
-
304
- // 3. Poll for result (adaptive: 5×1s + 5×2s + 5×3s = 30s max)
305
- for (var i = 0; i < 15; i++) {
306
- var delay = i < 5 ? 1000 : i < 10 ? 2000 : 3000;
307
- await new Promise(resolve => setTimeout(resolve, delay));
308
- var propResp = await client.get('/api/now/table/sys_properties', {
56
+ async function lookupDefinitionParams(client: any, defSysId: string): Promise<any[]> {
57
+ const paramTables = ['sys_hub_action_input', 'sys_hub_action_input_param', 'sys_hub_action_type_param'];
58
+ for (const table of paramTables) {
59
+ try {
60
+ const resp = await client.get('/api/now/table/' + table, {
309
61
  params: {
310
- sysparm_query: 'name=' + resultPropName,
311
- sysparm_fields: 'sys_id,value',
312
- sysparm_limit: 1
62
+ sysparm_query: 'model=' + defSysId + '^ORaction_type=' + defSysId,
63
+ sysparm_fields: 'sys_id,name,element,label,internal_type,type,type_label,order,mandatory,readonly,maxsize,data_structure,reference,reference_display,ref_qual,choice_option,column_name,default_value,use_dependent,dependent_on,internal_link,attributes,sys_class_name',
64
+ sysparm_limit: 50
313
65
  }
314
66
  });
315
- var propRecord = propResp.data.result?.[0];
316
- var propValue = propRecord?.value;
317
- if (propValue && propValue !== 'pending') {
318
- // Got result — clean up
319
- try { if (propRecord?.sys_id) await client.delete('/api/now/table/sys_properties/' + propRecord.sys_id); } catch (_) {}
320
- try { if (jobSysId) await client.delete('/api/now/table/sysauto_script/' + jobSysId); } catch (_) {}
321
- try {
322
- var parsed = JSON.parse(propValue);
323
- return {
324
- success: parsed.success,
325
- flowSysId: parsed.flow_sys_id,
326
- versionSysId: parsed.version_sys_id,
327
- tierUsed: parsed.tier_used,
328
- latestVersionSet: parsed.latest_version_set,
329
- latestVersionValue: parsed.latest_version_value,
330
- steps: parsed.steps,
331
- error: parsed.error
332
- };
333
- } catch (_) {
334
- return { success: false, error: 'Invalid JSON from scheduled job: ' + propValue.substring(0, 200) };
335
- }
336
- }
67
+ const results = resp.data.result;
68
+ if (results && results.length > 0) return results;
69
+ } catch (_) {}
70
+ }
71
+ return [];
72
+ }
73
+
74
+ function buildGraphQLInput(p: any, defaultValue?: any): any {
75
+ const name = p.name || p.element || '';
76
+ const type = p.internal_type || p.type || 'string';
77
+ const isMandatory = p.mandatory === 'true' || p.mandatory === true;
78
+ const order = parseInt(p.order) || 100;
79
+ const defVal = defaultValue !== undefined ? defaultValue : (p.default_value || '');
80
+ return {
81
+ id: p.sys_id || '',
82
+ name,
83
+ label: p.label || name,
84
+ internalType: type,
85
+ mandatory: isMandatory,
86
+ order,
87
+ valueSysId: '',
88
+ field_name: name,
89
+ type,
90
+ children: [],
91
+ displayValue: { value: '' },
92
+ value: type === 'conditions'
93
+ ? { schemaless: false, schemalessValue: '', value: '^EQ' }
94
+ : defVal
95
+ ? { schemaless: false, schemalessValue: '', value: String(defVal) }
96
+ : { value: '' },
97
+ parameter: {
98
+ id: p.sys_id || '',
99
+ label: p.label || name,
100
+ name,
101
+ type,
102
+ type_label: p.type_label || '',
103
+ order,
104
+ extended: false,
105
+ mandatory: isMandatory,
106
+ readonly: p.readonly === 'true' || p.readonly === true,
107
+ maxsize: parseInt(p.maxsize) || 80,
108
+ data_structure: p.data_structure || '',
109
+ reference: p.reference || '',
110
+ reference_display: p.reference_display || '',
111
+ ref_qual: p.ref_qual || '',
112
+ choiceOption: p.choice_option || '',
113
+ table: '',
114
+ columnName: p.column_name || '',
115
+ defaultValue: p.default_value || '',
116
+ use_dependent: p.use_dependent === 'true' || p.use_dependent === true,
117
+ dependent_on: p.dependent_on || '',
118
+ internal_link: p.internal_link || '',
119
+ show_ref_finder: false,
120
+ local: false,
121
+ attributes: p.attributes || '',
122
+ sys_class_name: p.sys_class_name || '',
123
+ children: []
337
124
  }
125
+ };
126
+ }
338
127
 
339
- // Timeout — clean up
128
+ async function addTriggerViaGraphQL(
129
+ client: any,
130
+ flowId: string,
131
+ triggerType: string,
132
+ table?: string,
133
+ condition?: string
134
+ ): Promise<{ success: boolean; triggerId?: string; steps?: any; error?: string }> {
135
+ const steps: any = {};
136
+
137
+ const triggerMap: Record<string, { type: string; name: string; triggerType: string; defNames: string[] }> = {
138
+ 'record_created': { type: 'record_create', name: 'Created', triggerType: 'Record',
139
+ defNames: ['sn_fd.trigger.record_create', 'global.sn_fd.trigger.record_create', 'sn_fd.trigger.record_created', 'global.sn_fd.trigger.record_created'] },
140
+ 'record_updated': { type: 'record_update', name: 'Updated', triggerType: 'Record',
141
+ defNames: ['sn_fd.trigger.record_update', 'global.sn_fd.trigger.record_update', 'sn_fd.trigger.record_updated', 'global.sn_fd.trigger.record_updated'] },
142
+ 'record_create_or_update': { type: 'record_create_or_update', name: 'Created or Updated', triggerType: 'Record',
143
+ defNames: ['sn_fd.trigger.record_create_or_update', 'global.sn_fd.trigger.record_create_or_update'] },
144
+ 'scheduled': { type: 'scheduled', name: 'Scheduled', triggerType: 'Scheduled',
145
+ defNames: ['sn_fd.trigger.scheduled', 'global.sn_fd.trigger.scheduled'] },
146
+ };
147
+
148
+ const config = triggerMap[triggerType] || triggerMap['record_create_or_update'];
149
+
150
+ let trigDefId: string | null = null;
151
+ for (const defName of config.defNames) {
340
152
  try {
341
- var cleanProp = await client.get('/api/now/table/sys_properties', {
342
- params: { sysparm_query: 'name=' + resultPropName, sysparm_fields: 'sys_id', sysparm_limit: 1 }
153
+ const resp = await client.get('/api/now/table/sys_hub_action_type_definition', {
154
+ params: { sysparm_query: 'internal_name=' + defName, sysparm_fields: 'sys_id', sysparm_limit: 1 }
343
155
  });
344
- if (cleanProp.data.result?.[0]?.sys_id) await client.delete('/api/now/table/sys_properties/' + cleanProp.data.result[0].sys_id);
156
+ trigDefId = resp.data.result?.[0]?.sys_id || null;
157
+ if (trigDefId) break;
345
158
  } catch (_) {}
346
- try { if (jobSysId) await client.delete('/api/now/table/sysauto_script/' + jobSysId); } catch (_) {}
347
- return { success: false, error: 'Scheduled job timed out after 30s — scheduler may be slow on this instance' };
348
- } catch (e: any) {
349
- return { success: false, error: 'Scheduled job setup failed: ' + (e.message || e) };
350
159
  }
351
- }
352
-
353
- // ── Business Rule compile: create a temp BR on sys_hub_flow to trigger engine compile ──
354
-
355
- /**
356
- * After the scheduled job creates the flow records, this function:
357
- * 1. Creates a temporary Business Rule on sys_hub_flow (after update)
358
- * 2. PATCHes the flow record to trigger the BR
359
- * 3. The BR runs in the Flow Designer engine context and tries to compile/publish
360
- * 4. Reads the result from a sys_property
361
- * 5. Cleans up both the BR and the property
362
- *
363
- * Why a BR instead of scheduled job? BRs on sys_hub_flow run in the Flow Designer
364
- * application scope, which may have access to APIs (like sn_fd.FlowDesigner) that
365
- * aren't available in global-scope scheduled jobs.
366
- */
367
- async function tryCompileViaBusinessRule(
368
- client: any,
369
- flowSysId: string,
370
- flowDescription: string
371
- ): Promise<{ success: boolean; result?: any; error?: string }> {
372
- var brResultProp = 'snow_flow.br_compile.' + Date.now();
373
-
374
- // The BR script: runs server-side in sys_hub_flow context
375
- // Probes all available APIs and tries compile/publish
376
- var brScript = [
377
- "(function executeRule(current, previous) {",
378
- " var PROP = '" + escForScript(brResultProp) + "';",
379
- " var r = { fired: true, context: 'business_rule', flow_id: current.sys_id + '' };",
380
- " try {",
381
- // Probe sn_fd namespace
382
- " if (typeof sn_fd !== 'undefined') {",
383
- " r.sn_fd = 'available';",
384
- " var snfdKeys = [];",
385
- " try { for (var k in sn_fd) { snfdKeys.push(k + ':' + typeof sn_fd[k]); } } catch(e) {}",
386
- " r.sn_fd_keys = snfdKeys;",
387
- "",
388
- // Try FlowDesigner (not available in scheduled job, might be available in BR)
389
- " if (typeof sn_fd.FlowDesigner !== 'undefined') {",
390
- " r.FlowDesigner = 'available';",
391
- " var fdMethods = [];",
392
- " try { for (var fm in sn_fd.FlowDesigner) { if (typeof sn_fd.FlowDesigner[fm] === 'function') fdMethods.push(fm); } } catch(e) {}",
393
- " r.FlowDesigner_methods = fdMethods;",
394
- " try {",
395
- " sn_fd.FlowDesigner.publishFlow(current.sys_id + '');",
396
- " r.publishFlow = 'success';",
397
- " } catch(pe) { r.publishFlow = (pe.getMessage ? pe.getMessage() : pe + '').substring(0, 200); }",
398
- " } else { r.FlowDesigner = 'unavailable'; }",
399
- "",
400
- // Try FlowAPI
401
- " if (typeof sn_fd.FlowAPI !== 'undefined') {",
402
- " r.FlowAPI = 'available';",
403
- " var faMethods = [];",
404
- " try { for (var am in sn_fd.FlowAPI) { if (typeof sn_fd.FlowAPI[am] === 'function') faMethods.push(am); } } catch(e) {}",
405
- " r.FlowAPI_methods = faMethods;",
406
- " try {",
407
- " var compRes = sn_fd.FlowAPI.compile(current.sys_id + '');",
408
- " r.compile = compRes ? (compRes + '').substring(0, 200) : 'success (null return)';",
409
- " } catch(ce) { r.compile = 'error: ' + (ce.getMessage ? ce.getMessage() : ce + '').substring(0, 200); }",
410
- " try {",
411
- " var pubRes = sn_fd.FlowAPI.publish(current.sys_id + '');",
412
- " r.publish = pubRes ? (pubRes + '').substring(0, 200) : 'success (null return)';",
413
- " } catch(pue) { r.publish = 'error: ' + (pue.getMessage ? pue.getMessage() : pue + '').substring(0, 200); }",
414
- " } else { r.FlowAPI = 'unavailable'; }",
415
- "",
416
- // Search for flow-related Script Includes
417
- " var si = new GlideRecord('sys_script_include');",
418
- " si.addQuery('name', 'CONTAINS', 'FlowDesigner');",
419
- " si.addOrCondition('name', 'CONTAINS', 'FlowCompiler');",
420
- " si.addOrCondition('name', 'CONTAINS', 'FlowPublish');",
421
- " si.addQuery('active', true);",
422
- " si.setLimit(15); si.query();",
423
- " r.script_includes = [];",
424
- " while (si.next()) {",
425
- " r.script_includes.push(si.getValue('name') + ' [api:' + si.getValue('api_name') + ', scope:' + si.getValue('sys_scope') + ']');",
426
- " }",
427
- "",
428
- // Try instantiating common Script Includes
429
- " var tryClasses = ['FlowDesignerScriptable', 'FlowDesignerService', 'FlowDesignerPublishService',",
430
- " 'FlowDesignerCompileService', 'FlowDesignerActionService', 'sn_fd.FlowDesignerScriptable'];",
431
- " r.script_include_tests = {};",
432
- " for (var tc = 0; tc < tryClasses.length; tc++) {",
433
- " try {",
434
- " var cls = eval(tryClasses[tc]);",
435
- " if (typeof cls === 'function') {",
436
- " var inst = new cls();",
437
- " r.script_include_tests[tryClasses[tc]] = 'instantiated';",
438
- // Try compile/publish methods on instance
439
- " if (typeof inst.compile === 'function') {",
440
- " try { inst.compile(current.sys_id + ''); r.script_include_tests[tryClasses[tc] + '.compile'] = 'success'; }",
441
- " catch(e) { r.script_include_tests[tryClasses[tc] + '.compile'] = (e + '').substring(0, 100); }",
442
- " }",
443
- " if (typeof inst.publishFlow === 'function') {",
444
- " try { inst.publishFlow(current.sys_id + ''); r.script_include_tests[tryClasses[tc] + '.publishFlow'] = 'success'; }",
445
- " catch(e) { r.script_include_tests[tryClasses[tc] + '.publishFlow'] = (e + '').substring(0, 100); }",
446
- " }",
447
- " } else { r.script_include_tests[tryClasses[tc]] = 'not a function: ' + typeof cls; }",
448
- " } catch(e) { r.script_include_tests[tryClasses[tc]] = 'error: ' + (e + '').substring(0, 100); }",
449
- " }",
450
- " } else { r.sn_fd = 'unavailable'; }",
451
- "",
452
- // Post-check: read back flow state
453
- " var post = new GlideRecord('sys_hub_flow');",
454
- " if (post.get(current.sys_id + '')) {",
455
- " r.post_latest_version = post.getValue('latest_version') + '';",
456
- " r.post_latest_snapshot = post.getValue('latest_snapshot') + '';",
457
- " r.post_status = post.getValue('status') + '';",
458
- " }",
459
- " } catch(topE) { r.error = (topE.getMessage ? topE.getMessage() : topE + '').substring(0, 300); }",
460
- " gs.setProperty(PROP, JSON.stringify(r));",
461
- "})(current, previous);"
462
- ].join('\n');
463
-
464
- try {
465
- // 1. Create result property
466
- await client.post('/api/now/table/sys_properties', {
467
- name: brResultProp,
468
- value: 'pending',
469
- type: 'string',
470
- description: 'Snow-Flow BR compile result (auto-cleanup)'
471
- });
472
-
473
- // 2. Create temporary Business Rule on sys_hub_flow
474
- var brResp = await client.post('/api/now/table/sys_script', {
475
- name: 'Snow-Flow Auto-Compile (temp-cleanup)',
476
- collection: 'sys_hub_flow',
477
- when: 'after',
478
- action_insert: false,
479
- action_update: true,
480
- action_delete: false,
481
- active: true,
482
- script: brScript,
483
- order: 10000,
484
- filter_condition: 'sys_id=' + flowSysId
485
- });
486
- var brSysId = brResp.data?.result?.sys_id;
487
-
488
- // 3. PATCH the flow record to trigger the BR (tiny description change)
489
- await new Promise(resolve => setTimeout(resolve, 1000)); // Let BR register
490
- await client.patch('/api/now/table/sys_hub_flow/' + flowSysId, {
491
- description: (flowDescription || '').trim() + ' '
492
- });
493
-
494
- // 4. Poll for result (5 attempts, 2s each = 10s max)
495
- for (var i = 0; i < 5; i++) {
496
- await new Promise(resolve => setTimeout(resolve, 2000));
497
- var propResp = await client.get('/api/now/table/sys_properties', {
160
+ if (!trigDefId) {
161
+ try {
162
+ const resp = await client.get('/api/now/table/sys_hub_action_type_definition', {
498
163
  params: {
499
- sysparm_query: 'name=' + brResultProp,
500
- sysparm_fields: 'sys_id,value',
501
- sysparm_limit: 1
164
+ sysparm_query: 'internal_nameSTARTSWITHsn_fd.trigger^internal_nameLIKE' + config.type,
165
+ sysparm_fields: 'sys_id,internal_name', sysparm_limit: 10
502
166
  }
503
167
  });
504
- var propRecord = propResp.data.result?.[0];
505
- var propValue = propRecord?.value;
506
-
507
- if (propValue && propValue !== 'pending') {
508
- // Got result — clean up
509
- try { if (propRecord?.sys_id) await client.delete('/api/now/table/sys_properties/' + propRecord.sys_id); } catch (_) {}
510
- try { if (brSysId) await client.delete('/api/now/table/sys_script/' + brSysId); } catch (_) {}
511
-
512
- try {
513
- return { success: true, result: JSON.parse(propValue) };
514
- } catch (_) {
515
- return { success: true, result: { raw: propValue.substring(0, 500) } };
516
- }
168
+ for (const r of (resp.data.result || [])) {
169
+ if ((r.internal_name || '').indexOf(config.type) > -1) { trigDefId = r.sys_id; break; }
517
170
  }
518
- }
519
-
520
- // Timeout — clean up
521
- try {
522
- var cleanProp = await client.get('/api/now/table/sys_properties', {
523
- params: { sysparm_query: 'name=' + brResultProp, sysparm_fields: 'sys_id', sysparm_limit: 1 }
524
- });
525
- if (cleanProp.data.result?.[0]?.sys_id) await client.delete('/api/now/table/sys_properties/' + cleanProp.data.result[0].sys_id);
526
171
  } catch (_) {}
527
- try { if (brSysId) await client.delete('/api/now/table/sys_script/' + brSysId); } catch (_) {}
528
-
529
- return { success: false, error: 'Business Rule did not fire within 10s — BR may not have registered or flow update was ignored' };
530
- } catch (e: any) {
531
- return { success: false, error: 'BR compile setup failed: ' + (e.message || e) };
532
172
  }
533
- }
173
+ if (!trigDefId) return { success: false, error: 'Trigger definition not found for: ' + triggerType, steps };
174
+ steps.def_lookup = { id: trigDefId };
534
175
 
535
- // ── Flow Factory (Scripted REST API bootstrap) ──────────────────────
536
-
537
- var FLOW_FACTORY_API_NAME = 'Snow-Flow Flow Factory';
538
- var FLOW_FACTORY_API_ID = 'flow_factory';
539
- var FLOW_FACTORY_CACHE_TTL = 300000; // 5 minutes
540
-
541
- var _flowFactoryCache: { apiSysId: string; namespace: string; timestamp: number } | null = null;
542
- var _bootstrapPromise: Promise<{ namespace: string; apiSysId: string }> | null = null;
543
-
544
- /**
545
- * ES5 GlideRecord script deployed as a Scripted REST API resource.
546
- * This runs server-side on ServiceNow and triggers all Business Rules,
547
- * unlike direct Table API inserts which skip sys_hub_flow_version creation.
548
- */
549
- /**
550
- * Discover script — GET /discover endpoint.
551
- * Probes which sn_fd APIs, methods and fields are available on this instance.
552
- */
553
- var FLOW_FACTORY_DISCOVER_SCRIPT = [
554
- '(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {',
555
- ' var r = {',
556
- ' build_tag: gs.getProperty("glide.buildtag") || "unknown",',
557
- ' build_name: gs.getProperty("glide.buildname") || "unknown",',
558
- ' apis: {}, methods: {}, fields: {}',
559
- ' };',
560
- ' try { r.apis.sn_fd = typeof sn_fd !== "undefined" ? "available" : "unavailable"; } catch(e) { r.apis.sn_fd = "unavailable"; }',
561
- ' var apiNames = ["FlowDesigner","FlowAPI","FlowPublisher","FlowCompiler"];',
562
- ' for (var i = 0; i < apiNames.length; i++) {',
563
- ' try {',
564
- ' if (typeof sn_fd !== "undefined") { r.apis[apiNames[i]] = typeof sn_fd[apiNames[i]]; }',
565
- ' else { r.apis[apiNames[i]] = "no_sn_fd"; }',
566
- ' } catch(e) { r.apis[apiNames[i]] = "error:" + e; }',
567
- ' }',
568
- ' var globalNames = ["GlideFlowDesigner","FlowDesignerInternalAPI","FlowDesignerAPI"];',
569
- ' for (var g = 0; g < globalNames.length; g++) {',
570
- ' try {',
571
- ' var gv = this[globalNames[g]];',
572
- ' r.apis[globalNames[g]] = gv ? typeof gv : "undefined";',
573
- ' } catch(e) { r.apis[globalNames[g]] = "error:" + e; }',
574
- ' }',
575
- ' try {',
576
- ' if (typeof sn_fd !== "undefined" && sn_fd.FlowDesigner) {',
577
- ' var fd = sn_fd.FlowDesigner;',
578
- ' var mns = ["createFlow","publishFlow","activateFlow","compileFlow","createDraftFlow","getFlow"];',
579
- ' for (var m = 0; m < mns.length; m++) { r.methods[mns[m]] = typeof fd[mns[m]]; }',
580
- ' }',
581
- ' } catch(e) { r.methods._error = e + ""; }',
582
- ' try {',
583
- ' var gr = new GlideRecord("sys_hub_flow_version");',
584
- ' var fns = ["compiled_definition","compile_state","is_current","published_flow","flow_definition"];',
585
- ' for (var f = 0; f < fns.length; f++) { r.fields[fns[f]] = gr.isValidField(fns[f]); }',
586
- ' } catch(e) { r.fields._error = e + ""; }',
587
- ' try {',
588
- ' var br = new GlideRecord("sys_script");',
589
- ' br.addQuery("collection","sys_hub_flow"); br.addQuery("active",true); br.query();',
590
- ' r.flow_br_count = br.getRowCount();',
591
- ' var brv = new GlideRecord("sys_script");',
592
- ' brv.addQuery("collection","sys_hub_flow_version"); brv.addQuery("active",true); brv.query();',
593
- ' r.version_br_count = brv.getRowCount();',
594
- ' } catch(e) { r.br_error = e + ""; }',
595
- ' response.setStatus(200);',
596
- ' response.setBody(r);',
597
- '})(request, response);'
598
- ].join('\n');
599
-
600
- /**
601
- * Create script — POST /create endpoint.
602
- * Three-tier approach:
603
- * Tier 1: sn_fd.FlowDesigner API (handles everything internally)
604
- * Tier 2: GlideRecord INSERT as draft → UPDATE to published (triggers compilation BRs)
605
- * Tier 3: Raw GlideRecord INSERT (last resort, may not register with engine)
606
- */
607
- var FLOW_FACTORY_SCRIPT = [
608
- '(function process(/*RESTAPIRequest*/ request, /*RESTAPIResponse*/ response) {',
609
- // ── Body parsing (3-method cascade) ──
610
- ' var body = null;',
611
- ' var parseLog = [];',
612
- ' try {',
613
- ' var ds = request.body.dataString;',
614
- ' if (ds) { body = JSON.parse(ds + ""); parseLog.push("dataString:ok"); }',
615
- ' } catch(e1) { parseLog.push("dataString:" + e1); }',
616
- ' if (!body) {',
617
- ' try {',
618
- ' var d = request.body.data;',
619
- ' if (d && typeof d === "object") { body = d; parseLog.push("data:ok"); }',
620
- ' else if (d) { body = JSON.parse(d + ""); parseLog.push("data-parse:ok"); }',
621
- ' } catch(e2) { parseLog.push("data:" + e2); }',
622
- ' }',
623
- ' if (!body) {',
624
- ' try { body = JSON.parse(request.body + ""); parseLog.push("body-direct:ok"); }',
625
- ' catch(e3) { parseLog.push("body-direct:" + e3); }',
626
- ' }',
627
- ' if (!body || typeof body !== "object") {',
628
- ' response.setStatus(400);',
629
- ' response.setBody({ success: false, error: "No parseable body", parseLog: parseLog, bodyType: typeof request.body });',
630
- ' return;',
631
- ' }',
632
- ' var result = { success: false, steps: {}, tier_used: null, parseLog: parseLog };',
633
- '',
634
- ' try {',
635
- ' var flowName = body.name || "Unnamed Flow";',
636
- ' var isSubflow = body.type === "subflow";',
637
- ' var flowDesc = body.description || flowName;',
638
- ' var flowCategory = body.category || "custom";',
639
- ' var runAs = body.run_as || "user";',
640
- ' var shouldActivate = body.activate !== false;',
641
- ' var triggerType = body.trigger_type || "manual";',
642
- ' var triggerTable = body.trigger_table || "";',
643
- ' var triggerCondition = body.trigger_condition || "";',
644
- ' var activities = body.activities || [];',
645
- ' var inputs = body.inputs || [];',
646
- ' var outputs = body.outputs || [];',
647
- ' var flowDef = body.flow_definition;',
648
- ' var flowDefStr = flowDef ? (typeof flowDef === "string" ? flowDef : JSON.stringify(flowDef)) : "";',
649
- ' var intName = flowName.toLowerCase().replace(/[^a-z0-9_]/g, "_").replace(/_+/g, "_").replace(/^_|_$/g, "");',
650
- '',
651
- ' var flowSysId = null;',
652
- ' var verSysId = null;',
653
- '',
654
- // ── TIER 1: sn_fd.FlowDesigner API ──
655
- ' try {',
656
- ' if (typeof sn_fd !== "undefined" && sn_fd.FlowDesigner && typeof sn_fd.FlowDesigner.createFlow === "function") {',
657
- ' var fdResult = sn_fd.FlowDesigner.createFlow({ name: flowName, description: flowDesc, type: isSubflow ? "subflow" : "flow", category: flowCategory, run_as: runAs });',
658
- ' if (fdResult) {',
659
- ' flowSysId = (typeof fdResult === "object" ? fdResult.sys_id || fdResult.getValue("sys_id") : fdResult) + "";',
660
- ' result.tier_used = "sn_fd_api";',
661
- ' result.steps.tier1 = { success: true, api: "sn_fd.FlowDesigner.createFlow" };',
662
- ' if (shouldActivate && typeof sn_fd.FlowDesigner.publishFlow === "function") {',
663
- ' try { sn_fd.FlowDesigner.publishFlow(flowSysId); result.steps.tier1_publish = { success: true }; }',
664
- ' catch(pe) { result.steps.tier1_publish = { success: false, error: pe + "" }; }',
665
- ' }',
666
- ' }',
667
- ' }',
668
- ' } catch(t1e) { result.steps.tier1 = { success: false, error: t1e.getMessage ? t1e.getMessage() : t1e + "" }; }',
669
- '',
670
- // ── TIER 2: GlideRecord INSERT as draft → UPDATE to published ──
671
- ' if (!flowSysId) {',
672
- ' try {',
673
- ' var flow = new GlideRecord("sys_hub_flow");',
674
- ' flow.initialize();',
675
- ' flow.setValue("name", flowName);',
676
- ' flow.setValue("description", flowDesc);',
677
- ' flow.setValue("internal_name", intName);',
678
- ' flow.setValue("category", flowCategory);',
679
- ' flow.setValue("run_as", runAs);',
680
- ' flow.setValue("active", false);',
681
- ' flow.setValue("status", "draft");',
682
- ' flow.setValue("validated", true);',
683
- ' flow.setValue("type", isSubflow ? "subflow" : "flow");',
684
- ' if (flowDefStr) { flow.setValue("flow_definition", flowDefStr); flow.setValue("latest_snapshot", flowDefStr); }',
685
- ' flowSysId = flow.insert();',
686
- ' result.steps.flow_insert = { success: !!flowSysId, sys_id: flowSysId + "", as_draft: true };',
687
- '',
688
- ' if (flowSysId) {',
689
- ' var ver = new GlideRecord("sys_hub_flow_version");',
690
- ' ver.initialize();',
691
- ' ver.setValue("flow", flowSysId);',
692
- ' ver.setValue("name", "1.0");',
693
- ' ver.setValue("version", "1.0");',
694
- ' ver.setValue("state", "draft");',
695
- ' ver.setValue("active", true);',
696
- ' ver.setValue("compile_state", "draft");',
697
- ' ver.setValue("is_current", true);',
698
- ' if (flowDefStr) ver.setValue("flow_definition", flowDefStr);',
699
- ' verSysId = ver.insert();',
700
- ' result.steps.version_insert = { success: !!verSysId, sys_id: verSysId + "", as_draft: true };',
701
- '',
702
- ' if (verSysId) {',
703
- ' var linkUpd = new GlideRecord("sys_hub_flow");',
704
- ' if (linkUpd.get(flowSysId)) { linkUpd.setValue("latest_version", verSysId); linkUpd.update(); }',
705
- ' }',
706
- '',
707
- ' if (shouldActivate) {',
708
- ' var flowPub = new GlideRecord("sys_hub_flow");',
709
- ' if (flowPub.get(flowSysId)) {',
710
- ' flowPub.setValue("status", "published");',
711
- ' flowPub.setValue("active", true);',
712
- ' flowPub.update();',
713
- ' result.steps.flow_publish_update = { success: true };',
714
- ' }',
715
- ' if (verSysId) {',
716
- ' var verPub = new GlideRecord("sys_hub_flow_version");',
717
- ' if (verPub.get(verSysId)) {',
718
- ' verPub.setValue("state", "published");',
719
- ' verPub.setValue("compile_state", "compiled");',
720
- ' verPub.setValue("published_flow", flowSysId);',
721
- ' verPub.update();',
722
- ' result.steps.version_publish_update = { success: true };',
723
- ' }',
724
- ' }',
725
- ' }',
726
- ' result.tier_used = "gliderecord_draft_then_publish";',
727
- ' result.success = true;',
728
- ' }',
729
- ' } catch(t2e) { result.steps.tier2 = { success: false, error: t2e.getMessage ? t2e.getMessage() : t2e + "" }; }',
730
- ' }',
731
- '',
732
- // ── TIER 3: Raw GlideRecord INSERT (last resort) ──
733
- ' if (!flowSysId) {',
734
- ' try {',
735
- ' var rawFlow = new GlideRecord("sys_hub_flow");',
736
- ' rawFlow.initialize();',
737
- ' rawFlow.setValue("name", flowName);',
738
- ' rawFlow.setValue("description", flowDesc);',
739
- ' rawFlow.setValue("internal_name", intName);',
740
- ' rawFlow.setValue("category", flowCategory);',
741
- ' rawFlow.setValue("run_as", runAs);',
742
- ' rawFlow.setValue("active", shouldActivate);',
743
- ' rawFlow.setValue("status", shouldActivate ? "published" : "draft");',
744
- ' rawFlow.setValue("validated", true);',
745
- ' rawFlow.setValue("type", isSubflow ? "subflow" : "flow");',
746
- ' if (flowDefStr) { rawFlow.setValue("flow_definition", flowDefStr); rawFlow.setValue("latest_snapshot", flowDefStr); }',
747
- ' flowSysId = rawFlow.insert();',
748
- ' if (flowSysId) {',
749
- ' var rawVer = new GlideRecord("sys_hub_flow_version");',
750
- ' rawVer.initialize();',
751
- ' rawVer.setValue("flow", flowSysId);',
752
- ' rawVer.setValue("name", "1.0"); rawVer.setValue("version", "1.0");',
753
- ' rawVer.setValue("state", shouldActivate ? "published" : "draft");',
754
- ' rawVer.setValue("active", true); rawVer.setValue("compile_state", "compiled");',
755
- ' rawVer.setValue("is_current", true);',
756
- ' if (shouldActivate) rawVer.setValue("published_flow", flowSysId);',
757
- ' if (flowDefStr) rawVer.setValue("flow_definition", flowDefStr);',
758
- ' verSysId = rawVer.insert();',
759
- ' if (verSysId) {',
760
- ' var rawLink = new GlideRecord("sys_hub_flow");',
761
- ' if (rawLink.get(flowSysId)) { rawLink.setValue("latest_version", verSysId); rawLink.update(); }',
762
- ' }',
763
- ' result.tier_used = "gliderecord_raw";',
764
- ' result.success = true;',
765
- ' }',
766
- ' } catch(t3e) { result.steps.tier3 = { success: false, error: t3e.getMessage ? t3e.getMessage() : t3e + "" }; }',
767
- ' }',
768
- '',
769
- // ── Common: trigger, actions, variables ──
770
- ' if (flowSysId) {',
771
- ' result.flow_sys_id = flowSysId + "";',
772
- ' result.version_sys_id = verSysId ? verSysId + "" : null;',
773
- ' result.version_created = !!verSysId;',
774
- '',
775
- ' if (!isSubflow && triggerType !== "manual") {',
776
- ' try {',
777
- ' var triggerMap = { "record_created": "sn_fd.trigger.record_created", "record_updated": "sn_fd.trigger.record_updated", "scheduled": "sn_fd.trigger.scheduled" };',
778
- ' var trigIntName = triggerMap[triggerType] || "";',
779
- ' if (trigIntName) {',
780
- ' var trigDef = new GlideRecord("sys_hub_action_type_definition");',
781
- ' trigDef.addQuery("internal_name", trigIntName); trigDef.query();',
782
- ' if (trigDef.next()) {',
783
- ' var trigInst = new GlideRecord("sys_hub_trigger_instance");',
784
- ' trigInst.initialize();',
785
- ' trigInst.setValue("flow", flowSysId); trigInst.setValue("action_type", trigDef.getUniqueValue());',
786
- ' trigInst.setValue("name", triggerType); trigInst.setValue("order", 0); trigInst.setValue("active", true);',
787
- ' if (triggerTable) trigInst.setValue("table", triggerTable);',
788
- ' if (triggerCondition) trigInst.setValue("condition", triggerCondition);',
789
- ' var trigSysId = trigInst.insert();',
790
- ' result.steps.trigger = { success: !!trigSysId, sys_id: trigSysId + "" };',
791
- ' } else { result.steps.trigger = { success: false, error: "Trigger def not found: " + trigIntName }; }',
792
- ' }',
793
- ' } catch(te) { result.steps.trigger = { success: false, error: te.getMessage ? te.getMessage() : te + "" }; }',
794
- ' }',
795
- '',
796
- ' var actionsCreated = 0;',
797
- ' for (var ai = 0; ai < activities.length; ai++) {',
798
- ' try {',
799
- ' var act = activities[ai];',
800
- ' var actDef = new GlideRecord("sys_hub_action_type_definition");',
801
- ' actDef.addQuery("internal_name", "CONTAINS", act.type || "script");',
802
- ' actDef.addOrCondition("name", "CONTAINS", act.type || "script"); actDef.query();',
803
- ' var actInst = new GlideRecord("sys_hub_action_instance");',
804
- ' actInst.initialize();',
805
- ' actInst.setValue("flow", flowSysId); actInst.setValue("name", act.name || "Action " + (ai + 1));',
806
- ' actInst.setValue("order", (ai + 1) * 100); actInst.setValue("active", true);',
807
- ' if (actDef.next()) actInst.setValue("action_type", actDef.getUniqueValue());',
808
- ' if (actInst.insert()) actionsCreated++;',
809
- ' } catch(ae) {}',
810
- ' }',
811
- ' result.steps.actions = { success: true, created: actionsCreated, requested: activities.length };',
812
- '',
813
- ' var varsCreated = 0;',
814
- ' if (isSubflow) {',
815
- ' for (var vi = 0; vi < inputs.length; vi++) {',
816
- ' try { var inp = inputs[vi]; var fv = new GlideRecord("sys_hub_flow_variable"); fv.initialize();',
817
- ' fv.setValue("flow",flowSysId); fv.setValue("name",inp.name); fv.setValue("label",inp.label||inp.name);',
818
- ' fv.setValue("type",inp.type||"string"); fv.setValue("mandatory",inp.mandatory||false);',
819
- ' fv.setValue("default_value",inp.default_value||""); fv.setValue("variable_type","input");',
820
- ' if (fv.insert()) varsCreated++;',
821
- ' } catch(ve) {}',
822
- ' }',
823
- ' for (var vo = 0; vo < outputs.length; vo++) {',
824
- ' try { var out = outputs[vo]; var ov = new GlideRecord("sys_hub_flow_variable"); ov.initialize();',
825
- ' ov.setValue("flow",flowSysId); ov.setValue("name",out.name); ov.setValue("label",out.label||out.name);',
826
- ' ov.setValue("type",out.type||"string"); ov.setValue("variable_type","output");',
827
- ' if (ov.insert()) varsCreated++;',
828
- ' } catch(ve) {}',
829
- ' }',
830
- ' }',
831
- ' result.steps.variables = { success: true, created: varsCreated };',
832
- ' }',
833
- '',
834
- ' if (result.success) response.setStatus(201);',
835
- ' else response.setStatus(500);',
836
- '',
837
- ' } catch (e) {',
838
- ' result.success = false;',
839
- ' result.error = e.getMessage ? e.getMessage() : e + "";',
840
- ' response.setStatus(500);',
841
- ' }',
842
- ' response.setBody(result);',
843
- '})(request, response);'
844
- ].join('\n');
845
-
846
- /**
847
- * Resolve the REST API namespace for a sys_ws_definition record.
848
- *
849
- * Collects namespace candidates from multiple sources, then VERIFIES each
850
- * one via HTTP before accepting it. This prevents returning invalid values
851
- * like scope sys_ids or numeric identifiers that aren't valid URL namespaces.
852
- *
853
- * Verification: GET /api/{candidate}/{api_id}/discover
854
- * - 200 = correct namespace, /discover endpoint works
855
- * - 401/403 = correct namespace, auth issue
856
- * - 405 = correct namespace, wrong HTTP method (endpoint exists)
857
- * - 400/404 = wrong namespace or API not registered yet
858
- */
859
- async function resolveFactoryNamespace(
860
- client: any,
861
- apiSysId: string,
862
- instanceUrl: string
863
- ): Promise<string | null> {
864
- // ── Collect namespace candidates (ordered by likelihood) ──
865
- var candidates: string[] = [];
176
+ const params = await lookupDefinitionParams(client, trigDefId);
177
+ steps.params_found = params.length;
866
178
 
867
- // Most common for PDI / global scope custom APIs
868
- candidates.push('global');
179
+ const gqlInputs = params.map((p: any) => buildGraphQLInput(p));
869
180
 
870
- // Dot-walk to sys_scope.scope
871
- try {
872
- var dotWalkResp = await client.get('/api/now/table/sys_ws_definition/' + apiSysId, {
873
- params: {
874
- sysparm_fields: 'namespace.scope',
875
- sysparm_display_value: 'false'
876
- }
877
- });
878
- var scopeStr = dotWalkResp.data.result?.['namespace.scope'];
879
- if (scopeStr && typeof scopeStr === 'string' && scopeStr.length > 1) {
880
- candidates.push(scopeStr);
881
- }
882
- } catch (_) {}
883
-
884
- // Read namespace display_value (might be scope string itself)
885
- try {
886
- var nsResp = await client.get('/api/now/table/sys_ws_definition/' + apiSysId, {
887
- params: { sysparm_fields: 'namespace', sysparm_display_value: 'true' }
888
- });
889
- var nsDisplay = nsResp.data.result?.namespace;
890
- if (typeof nsDisplay === 'string' && nsDisplay.length > 1) {
891
- candidates.push(nsDisplay === 'Global' ? 'global' : nsDisplay);
892
- }
893
- } catch (_) {}
894
-
895
- // OOB namespace
896
- candidates.push('now');
181
+ if (gqlInputs.length === 0 && config.triggerType === 'Record') {
182
+ gqlInputs.push(
183
+ buildGraphQLInput({ name: 'table', label: 'Table', internal_type: 'table_name', mandatory: 'true', order: '1' }),
184
+ buildGraphQLInput({ name: 'condition', label: 'Condition', internal_type: 'conditions', mandatory: 'false', order: '100', use_dependent: 'true', dependent_on: 'table' })
185
+ );
186
+ }
897
187
 
898
- // Company code from sys_properties
188
+ const triggerResponseFields = 'triggerInstances { inserts { sysId uiUniqueIdentifier __typename } updates deletes __typename }';
899
189
  try {
900
- var compResp = await client.get('/api/now/table/sys_properties', {
901
- params: {
902
- sysparm_query: 'name=glide.appcreator.company.code',
903
- sysparm_fields: 'value',
904
- sysparm_limit: 1
190
+ const insertResult = await executeFlowPatchMutation(client, {
191
+ flowId: flowId,
192
+ triggerInstances: {
193
+ insert: [{
194
+ flowSysId: flowId,
195
+ name: config.name,
196
+ triggerType: config.triggerType,
197
+ triggerDefinitionId: trigDefId,
198
+ type: config.type,
199
+ hasDynamicOutputs: false,
200
+ metadata: '{"predicates":[]}',
201
+ inputs: gqlInputs,
202
+ outputs: []
203
+ }]
204
+ }
205
+ }, triggerResponseFields);
206
+
207
+ const triggerId = insertResult?.triggerInstances?.inserts?.[0]?.sysId;
208
+ steps.insert = { success: !!triggerId, triggerId };
209
+ if (!triggerId) return { success: false, steps, error: 'GraphQL trigger INSERT returned no trigger ID' };
210
+
211
+ if (table) {
212
+ const updateInputs: any[] = [
213
+ {
214
+ name: 'table',
215
+ displayField: 'number',
216
+ displayValue: { schemaless: false, schemalessValue: '', value: table.charAt(0).toUpperCase() + table.slice(1) },
217
+ value: { schemaless: false, schemalessValue: '', value: table }
218
+ },
219
+ {
220
+ name: 'condition',
221
+ displayValue: { schemaless: false, schemalessValue: '', value: condition || '^EQ' }
222
+ }
223
+ ];
224
+ try {
225
+ await executeFlowPatchMutation(client, {
226
+ flowId: flowId,
227
+ triggerInstances: { update: [{ id: triggerId, inputs: updateInputs }] }
228
+ }, triggerResponseFields);
229
+ steps.update = { success: true, table, condition: condition || '^EQ' };
230
+ } catch (e: any) {
231
+ steps.update = { success: false, error: e.message };
905
232
  }
906
- });
907
- var companyCode = compResp.data.result?.[0]?.value;
908
- if (companyCode) candidates.push(companyCode);
909
- } catch (_) {}
910
-
911
- // Instance subdomain (e.g. "dev354059")
912
- try {
913
- var match = instanceUrl.match(/https?:\/\/([^.]+)\./);
914
- if (match && match[1]) candidates.push(match[1]);
915
- } catch (_) {}
916
-
917
- // ── Deduplicate ──
918
- var seen: Record<string, boolean> = {};
919
- var unique: string[] = [];
920
- for (var i = 0; i < candidates.length; i++) {
921
- var lower = candidates[i].toLowerCase();
922
- if (!seen[lower]) {
923
- seen[lower] = true;
924
- unique.push(lower);
925
233
  }
926
- }
927
234
 
928
- // ── Verify each candidate via HTTP ──
929
- for (var j = 0; j < unique.length; j++) {
930
- var ns = unique[j];
931
- // Check 1: GET /discover (v5 endpoint, returns 200 with discovery data)
932
- try {
933
- var discoverResp = await client.get('/api/' + ns + '/' + FLOW_FACTORY_API_ID + '/discover');
934
- if (discoverResp.status === 200 || discoverResp.data) return ns;
935
- } catch (discoverErr: any) {
936
- var ds = discoverErr.response?.status;
937
- if (ds === 401 || ds === 403 || ds === 405) return ns;
938
- // 400/404 = wrong namespace or not registered yet
939
- }
940
- // Check 2: GET /create (POST-only, expect 405 for correct namespace)
941
- try {
942
- await client.get('/api/' + ns + '/' + FLOW_FACTORY_API_ID + '/create');
943
- return ns; // 200 = unexpected but valid
944
- } catch (createErr: any) {
945
- var cs = createErr.response?.status;
946
- if (cs === 405 || cs === 401 || cs === 403) return ns;
947
- // 400/404 = wrong namespace (ServiceNow may return 400 instead of 405)
948
- }
949
- // Check 3: POST /create with empty body — distinguishes "wrong namespace" from
950
- // "correct namespace but script validation error". Wrong namespace returns
951
- // 400 with "Requested URI does not represent any resource". Our script returns
952
- // a different error body (e.g. {success:false, error:"..."}).
953
- try {
954
- await client.post('/api/' + ns + '/' + FLOW_FACTORY_API_ID + '/create', {});
955
- return ns; // Unexpected success, namespace confirmed
956
- } catch (postErr: any) {
957
- var pe = postErr.response;
958
- if (!pe) continue;
959
- if (pe.status === 401 || pe.status === 403) return ns;
960
- // Check error body — "Requested URI" = wrong namespace, anything else = our script
961
- var errStr = JSON.stringify(pe.data || '');
962
- if (!errStr.includes('Requested URI')) return ns;
963
- }
235
+ return { success: true, triggerId, steps };
236
+ } catch (e: any) {
237
+ steps.insert = { success: false, error: e.message };
238
+ return { success: false, steps, error: 'GraphQL trigger INSERT failed: ' + e.message };
964
239
  }
965
-
966
- return null; // No candidate verified — API may not be registered yet
967
240
  }
968
241
 
969
- /**
970
- * Ensure the Flow Factory Scripted REST API exists on the ServiceNow instance.
971
- * Idempotent — checks cache first, then instance, deploys only if missing.
972
- *
973
- * Namespace resolution strategy:
974
- * 1. Dot-walk to sys_scope.scope from the API record (deterministic, no HTTP probing)
975
- * 2. Read namespace field with display_value=all (fallback)
976
- * 3. Company code from sys_properties (fallback)
977
- * 4. HTTP probing to /discover and /create endpoints (last resort)
978
- *
979
- * Stale API detection: if the API exists but has no /discover endpoint
980
- * (created by an older tool version), it is deleted and redeployed.
981
- */
982
- async function ensureFlowFactoryAPI(
242
+ async function addActionViaGraphQL(
983
243
  client: any,
984
- instanceUrl: string
985
- ): Promise<{ namespace: string; apiSysId: string }> {
986
- // 1. Check in-memory cache
987
- if (_flowFactoryCache && (Date.now() - _flowFactoryCache.timestamp) < FLOW_FACTORY_CACHE_TTL) {
988
- return { namespace: _flowFactoryCache.namespace, apiSysId: _flowFactoryCache.apiSysId };
989
- }
990
-
991
- // 2. Concurrency lock — reuse in-flight bootstrap
992
- if (_bootstrapPromise) {
993
- return _bootstrapPromise;
244
+ flowId: string,
245
+ actionType: string,
246
+ actionName: string,
247
+ inputs?: Record<string, string>,
248
+ order?: number
249
+ ): Promise<{ success: boolean; actionId?: string; steps?: any; error?: string }> {
250
+ const steps: any = {};
251
+
252
+ const actionTypeNames: Record<string, string[]> = {
253
+ 'log': ['sn_fd.action.log', 'global.sn_fd.action.log', 'global.log'],
254
+ 'create_record': ['sn_fd.action.create_record', 'global.sn_fd.action.create_record'],
255
+ 'update_record': ['sn_fd.action.update_record', 'global.sn_fd.action.update_record'],
256
+ 'notification': ['sn_fd.action.send_notification', 'global.sn_fd.action.send_notification'],
257
+ 'script': ['sn_fd.action.script', 'global.sn_fd.action.script', 'global.script_action'],
258
+ 'field_update': ['sn_fd.action.field_update', 'global.sn_fd.action.field_update'],
259
+ 'wait': ['sn_fd.action.wait', 'global.sn_fd.action.wait'],
260
+ 'approval': ['sn_fd.action.create_approval', 'global.sn_fd.action.create_approval'],
261
+ };
262
+
263
+ let actionDefId: string | null = null;
264
+ const candidates = actionTypeNames[actionType] || [];
265
+ for (const name of candidates) {
266
+ try {
267
+ const resp = await client.get('/api/now/table/sys_hub_action_type_definition', {
268
+ params: { sysparm_query: 'internal_name=' + name, sysparm_fields: 'sys_id', sysparm_limit: 1 }
269
+ });
270
+ actionDefId = resp.data.result?.[0]?.sys_id || null;
271
+ if (actionDefId) break;
272
+ } catch (_) {}
994
273
  }
995
-
996
- _bootstrapPromise = (async () => {
274
+ if (!actionDefId) {
997
275
  try {
998
- // 3. Check if API already exists on instance
999
- var checkResp = await client.get('/api/now/table/sys_ws_definition', {
276
+ const resp = await client.get('/api/now/table/sys_hub_action_type_definition', {
1000
277
  params: {
1001
- sysparm_query: 'api_id=' + FLOW_FACTORY_API_ID,
1002
- sysparm_fields: 'sys_id,api_id,namespace',
1003
- sysparm_limit: 1
278
+ sysparm_query: 'internal_nameLIKE' + actionType + '^ORnameLIKE' + actionType,
279
+ sysparm_fields: 'sys_id,internal_name', sysparm_limit: 5
1004
280
  }
1005
281
  });
282
+ actionDefId = resp.data.result?.[0]?.sys_id || null;
283
+ } catch (_) {}
284
+ }
285
+ if (!actionDefId) return { success: false, error: 'Action definition not found for: ' + actionType, steps };
286
+ steps.def_lookup = { id: actionDefId };
1006
287
 
1007
- if (checkResp.data.result && checkResp.data.result.length > 0) {
1008
- var existing = checkResp.data.result[0];
1009
- var ns = await resolveFactoryNamespace(client, existing.sys_id, instanceUrl);
1010
-
1011
- if (ns) {
1012
- // Verify the API has v5 endpoints (check /discover exists)
1013
- var hasDiscover = false;
1014
- try {
1015
- var verifyResp = await client.get('/api/' + ns + '/' + FLOW_FACTORY_API_ID + '/discover');
1016
- hasDiscover = verifyResp.status === 200 || !!verifyResp.data;
1017
- } catch (verifyErr: any) {
1018
- var vs = verifyErr.response?.status;
1019
- // 401/403 = endpoint exists but auth issue; 405 = exists but wrong method
1020
- hasDiscover = vs === 401 || vs === 403 || vs === 405;
1021
- }
1022
-
1023
- if (hasDiscover) {
1024
- _flowFactoryCache = { apiSysId: existing.sys_id, namespace: ns, timestamp: Date.now() };
1025
- return { namespace: ns, apiSysId: existing.sys_id };
1026
- }
1027
- // /discover missing → stale API from older version, fall through to delete
1028
- }
1029
-
1030
- // Delete stale API and redeploy with current scripts
1031
- invalidateFlowFactoryCache();
1032
- try {
1033
- await client.delete('/api/now/table/sys_ws_definition/' + existing.sys_id);
1034
- } catch (_) {
1035
- // If delete fails, try deployment anyway — will error on duplicate api_id
1036
- }
1037
- // Brief pause to let ServiceNow finalize the delete
1038
- await new Promise(resolve => setTimeout(resolve, 1000));
1039
- }
288
+ const params = await lookupDefinitionParams(client, actionDefId);
289
+ steps.params_found = params.length;
1040
290
 
1041
- // 4. Deploy the Scripted REST API (do NOT set namespace — let ServiceNow assign it)
1042
- var apiResp = await client.post('/api/now/table/sys_ws_definition', {
1043
- name: FLOW_FACTORY_API_NAME,
1044
- api_id: FLOW_FACTORY_API_ID,
1045
- active: true,
1046
- short_description: 'Bootstrapped by Snow-Flow MCP for reliable Flow Designer creation via GlideRecord',
1047
- is_versioned: false,
1048
- enforce_acl: 'false',
1049
- requires_authentication: true
1050
- });
291
+ const gqlInputs = params.map((p: any) => {
292
+ const pName = p.name || p.element || '';
293
+ const providedValue = inputs?.[pName];
294
+ return buildGraphQLInput(p, providedValue);
295
+ });
1051
296
 
1052
- var apiSysId = apiResp.data.result?.sys_id;
1053
- if (!apiSysId) {
1054
- throw new Error('Failed to create Scripted REST API definition — no sys_id returned');
297
+ const uuid = generateUUID();
298
+ const actionResponseFields = 'actions { inserts { sysId uiUniqueIdentifier __typename } updates deletes __typename }';
299
+ try {
300
+ const result = await executeFlowPatchMutation(client, {
301
+ flowId: flowId,
302
+ actions: {
303
+ insert: [{
304
+ actionTypeSysId: actionDefId,
305
+ metadata: '{"predicates":[]}',
306
+ flowSysId: flowId,
307
+ generationSource: '',
308
+ order: String(order || 1),
309
+ parent: '',
310
+ uiUniqueIdentifier: uuid,
311
+ type: 'action',
312
+ parentUiId: '',
313
+ inputs: gqlInputs
314
+ }]
1055
315
  }
316
+ }, actionResponseFields);
1056
317
 
1057
- // 5a. Deploy the POST /create resource
1058
- try {
1059
- await client.post('/api/now/table/sys_ws_operation', {
1060
- web_service_definition: apiSysId,
1061
- http_method: 'POST',
1062
- name: 'create',
1063
- active: true,
1064
- relative_path: '/create',
1065
- short_description: 'Create a flow/subflow via 3-tier approach: sn_fd API → draft+publish → raw GlideRecord',
1066
- operation_script: FLOW_FACTORY_SCRIPT,
1067
- requires_authentication: true,
1068
- enforce_acl: 'false'
1069
- });
1070
- } catch (opError: any) {
1071
- // Cleanup the API definition if operation creation fails
1072
- try { await client.delete('/api/now/table/sys_ws_definition/' + apiSysId); } catch (_) {}
1073
- throw new Error('Failed to create Scripted REST operation: ' + (opError.message || opError));
1074
- }
318
+ const actionId = result?.actions?.inserts?.[0]?.sysId;
319
+ steps.insert = { success: !!actionId, actionId, uuid };
1075
320
 
1076
- // 5b. Deploy the GET /discover resource (API capability probing)
321
+ if (actionId && inputs && Object.keys(inputs).length > 0) {
322
+ const updateInputs = Object.entries(inputs).map(([name, value]) => ({
323
+ name,
324
+ value: { schemaless: false, schemalessValue: '', value: String(value) }
325
+ }));
1077
326
  try {
1078
- await client.post('/api/now/table/sys_ws_operation', {
1079
- web_service_definition: apiSysId,
1080
- http_method: 'GET',
1081
- name: 'discover',
1082
- active: true,
1083
- relative_path: '/discover',
1084
- short_description: 'Probe available sn_fd APIs, methods and fields for this ServiceNow instance',
1085
- operation_script: FLOW_FACTORY_DISCOVER_SCRIPT,
1086
- requires_authentication: true,
1087
- enforce_acl: 'false'
1088
- });
1089
- } catch (_) {
1090
- // Non-fatal: create endpoint is more important than discover
1091
- }
1092
-
1093
- // 6. Wait for ServiceNow REST framework to register endpoints
1094
- await new Promise(resolve => setTimeout(resolve, 4000));
1095
-
1096
- // 7. Resolve namespace via HTTP-verified probing
1097
- var resolvedNs = await resolveFactoryNamespace(client, apiSysId, instanceUrl);
1098
-
1099
- // 8. If resolution failed, wait longer and retry (some instances are slow)
1100
- if (!resolvedNs) {
1101
- await new Promise(resolve => setTimeout(resolve, 5000));
1102
- resolvedNs = await resolveFactoryNamespace(client, apiSysId, instanceUrl);
1103
- }
1104
- if (!resolvedNs) {
1105
- throw new Error('Flow Factory deployed (sys_id=' + apiSysId + ') but no namespace candidate could be verified via HTTP after 9s. Candidates tried: global, dot-walk scope, display_value, now, company code, subdomain.');
327
+ await executeFlowPatchMutation(client, {
328
+ flowId: flowId,
329
+ actions: { update: [{ uiUniqueIdentifier: uuid, type: 'action', inputs: updateInputs }] }
330
+ }, actionResponseFields);
331
+ steps.value_update = { success: true };
332
+ } catch (e: any) {
333
+ steps.value_update = { success: false, error: e.message };
1106
334
  }
1107
-
1108
- _flowFactoryCache = { apiSysId: apiSysId, namespace: resolvedNs, timestamp: Date.now() };
1109
- return { namespace: resolvedNs, apiSysId: apiSysId };
1110
-
1111
- } finally {
1112
- _bootstrapPromise = null;
1113
335
  }
1114
- })();
1115
-
1116
- return _bootstrapPromise;
1117
- }
1118
-
1119
- /**
1120
- * Invalidate the Flow Factory cache (e.g. on 404 when API was deleted externally).
1121
- */
1122
- function invalidateFlowFactoryCache(): void {
1123
- _flowFactoryCache = null;
1124
- _discoveryCache = null;
1125
- }
1126
-
1127
- /**
1128
- * Call the /discover endpoint on the Flow Factory API to learn which
1129
- * sn_fd APIs and methods are available on the target ServiceNow instance.
1130
- * Results are cached alongside the factory cache.
1131
- */
1132
- var _discoveryCache: any = null;
1133
336
 
1134
- async function discoverInstanceCapabilities(
1135
- client: any,
1136
- namespace: string
1137
- ): Promise<any> {
1138
- if (_discoveryCache) return _discoveryCache;
1139
- try {
1140
- var discoverEndpoint = '/api/' + namespace + '/' + FLOW_FACTORY_API_ID + '/discover';
1141
- var resp = await client.get(discoverEndpoint);
1142
- _discoveryCache = resp.data?.result || resp.data || null;
1143
- return _discoveryCache;
1144
- } catch (_) {
1145
- return null;
337
+ return { success: true, actionId: actionId || undefined, steps };
338
+ } catch (e: any) {
339
+ steps.insert = { success: false, error: e.message };
340
+ return { success: false, steps, error: 'GraphQL action INSERT failed: ' + e.message };
1146
341
  }
1147
342
  }
1148
343
 
1149
- /**
1150
- * After creating a flow (via any method), try to register it with the
1151
- * Flow Designer engine by calling its built-in compile / publish / activate
1152
- * REST endpoints. Without this step the flow record exists in the DB but
1153
- * Flow Designer cannot open it ("Your flow cannot be found").
1154
- *
1155
- * Tries multiple known endpoint patterns because the namespace/path changed
1156
- * across ServiceNow releases. Returns diagnostics on which attempts were
1157
- * made and what succeeded.
1158
- */
1159
- async function registerFlowWithEngine(
344
+ async function createFlowViaProcessFlowAPI(
1160
345
  client: any,
1161
- flowSysId: string,
1162
- shouldActivate: boolean
1163
- ): Promise<{ success: boolean; method: string; attempts: string[] }> {
1164
- var attempts: string[] = [];
1165
-
1166
- // Helper: try a POST and classify the result (capture error body for diagnostics)
1167
- async function tryPost(label: string, url: string, body?: any): Promise<boolean> {
1168
- try {
1169
- await client.post(url, body || {});
1170
- attempts.push(label + ': success');
1171
- return true;
1172
- } catch (e: any) {
1173
- var s = e.response?.status || 'err';
1174
- var errBody = '';
1175
- try { errBody = JSON.stringify(e.response?.data || '').substring(0, 150); } catch (_) {}
1176
- attempts.push(label + ': ' + s + (errBody ? ' ' + errBody : ''));
1177
- return false;
1178
- }
346
+ params: {
347
+ name: string;
348
+ description: string;
349
+ isSubflow: boolean;
350
+ runAs: string;
351
+ shouldActivate: boolean;
1179
352
  }
353
+ ): Promise<{
354
+ success: boolean;
355
+ flowSysId?: string;
356
+ versionCreated?: boolean;
357
+ flowData?: any;
358
+ error?: string;
359
+ }> {
360
+ try {
361
+ var flowResp = await client.post(
362
+ '/api/now/processflow/flow',
363
+ {
364
+ access: 'public',
365
+ description: params.description || '',
366
+ flowPriority: 'MEDIUM',
367
+ name: params.name,
368
+ protection: '',
369
+ runAs: params.runAs || 'user',
370
+ runWithRoles: { value: '', displayValue: '' },
371
+ scope: 'global',
372
+ scopeDisplayName: '',
373
+ scopeName: '',
374
+ security: { can_read: true, can_write: true },
375
+ status: 'draft',
376
+ type: params.isSubflow ? 'subflow' : 'flow',
377
+ userHasRolesAssignedToFlow: true,
378
+ active: false,
379
+ deleted: false
380
+ },
381
+ {
382
+ params: {
383
+ param_only_properties: 'true',
384
+ sysparm_transaction_scope: 'global'
385
+ }
386
+ }
387
+ );
1180
388
 
1181
- // ── Strategy 1: Publish via Flow Designer REST API ───────────────
1182
- // This is the closest equivalent to clicking "Publish" in the UI.
1183
- var publishPaths = [
1184
- '/api/sn_fd/flow/' + flowSysId + '/publish',
1185
- '/api/sn_fd/designer/flow/' + flowSysId + '/publish',
1186
- '/api/sn_flow_designer/flow/' + flowSysId + '/publish',
1187
- ];
1188
- for (var pi = 0; pi < publishPaths.length; pi++) {
1189
- if (await tryPost('publish[' + pi + ']', publishPaths[pi])) {
1190
- return { success: true, method: 'publish', attempts: attempts };
389
+ var flowResult = flowResp.data?.result?.data;
390
+ if (!flowResult?.id) {
391
+ var errDetail = flowResp.data?.result?.errorMessage || 'no flow id returned';
392
+ return { success: false, error: 'ProcessFlow API: ' + errDetail };
1191
393
  }
1192
- }
1193
394
 
1194
- // ── Strategy 2: Activate (registers the flow with the engine) ────
1195
- if (shouldActivate) {
1196
- var activatePaths = [
1197
- '/api/sn_fd/flow/' + flowSysId + '/activate',
1198
- '/api/sn_fd/designer/flow/' + flowSysId + '/activate',
1199
- '/api/sn_flow_designer/flow/' + flowSysId + '/activate',
1200
- ];
1201
- for (var ai = 0; ai < activatePaths.length; ai++) {
1202
- if (await tryPost('activate[' + ai + ']', activatePaths[ai])) {
1203
- return { success: true, method: 'activate', attempts: attempts };
1204
- }
1205
- }
1206
- }
395
+ var flowSysId = flowResult.id;
1207
396
 
1208
- // ── Strategy 3: Checkout + checkin (triggers internal compilation) ─
1209
- var checkoutPaths = [
1210
- '/api/sn_fd/flow/' + flowSysId + '/checkout',
1211
- '/api/sn_fd/designer/flow/' + flowSysId + '/checkout',
1212
- ];
1213
- for (var ci = 0; ci < checkoutPaths.length; ci++) {
1214
- if (await tryPost('checkout[' + ci + ']', checkoutPaths[ci])) {
1215
- var checkinPath = checkoutPaths[ci].replace('/checkout', '/checkin');
1216
- await tryPost('checkin[' + ci + ']', checkinPath);
1217
- return { success: true, method: 'checkout+checkin', attempts: attempts };
397
+ var versionCreated = false;
398
+ try {
399
+ await client.post(
400
+ '/api/now/processflow/versioning/create_version',
401
+ { item_sys_id: flowSysId, type: 'Autosave', annotation: '', favorite: false },
402
+ { params: { sysparm_transaction_scope: 'global' } }
403
+ );
404
+ versionCreated = true;
405
+ } catch (_) {
1218
406
  }
1219
- }
1220
407
 
1221
- // ── Strategy 4: Snapshot (already existed triggers version snapshot) ─
1222
- if (await tryPost('snapshot', '/api/sn_flow_designer/flow/snapshot', { flow_id: flowSysId })) {
1223
- return { success: true, method: 'snapshot', attempts: attempts };
1224
- }
1225
- // Try alternate snapshot path
1226
- if (await tryPost('snapshot-alt', '/api/sn_fd/flow/' + flowSysId + '/snapshot')) {
1227
- return { success: true, method: 'snapshot-alt', attempts: attempts };
408
+ return { success: true, flowSysId, versionCreated, flowData: flowResult };
409
+ } catch (e: any) {
410
+ var msg = e.message || '';
411
+ try { msg += ' — ' + JSON.stringify(e.response?.data || '').substring(0, 200); } catch (_) {}
412
+ return { success: false, error: 'ProcessFlow API: ' + msg };
1228
413
  }
1229
-
1230
- return { success: false, method: 'none', attempts: attempts };
1231
414
  }
1232
415
 
1233
- /**
1234
- * Resolve a flow name to its sys_id. If the value is already a 32-char hex
1235
- * string it is returned as-is.
1236
- */
416
+ // ── resolve helpers ───────────────────────────────────────────────────
417
+
1237
418
  async function resolveFlowId(client: any, flowId: string): Promise<string> {
1238
419
  if (isSysId(flowId)) return flowId;
1239
420
 
@@ -1268,17 +449,15 @@ export const toolDefinition: MCPToolDefinition = {
1268
449
  properties: {
1269
450
  action: {
1270
451
  type: 'string',
1271
- enum: ['create', 'create_subflow', 'list', 'get', 'update', 'activate', 'deactivate', 'delete', 'publish'],
452
+ enum: ['create', 'create_subflow', 'list', 'get', 'update', 'activate', 'deactivate', 'delete', 'publish', 'add_trigger', 'add_action'],
1272
453
  description: 'Action to perform'
1273
454
  },
1274
455
 
1275
- // ── shared identifiers ──
1276
456
  flow_id: {
1277
457
  type: 'string',
1278
458
  description: 'Flow sys_id or name (required for get, update, activate, deactivate, delete, publish)'
1279
459
  },
1280
460
 
1281
- // ── create / create_subflow params ──
1282
461
  name: {
1283
462
  type: 'string',
1284
463
  description: 'Flow name (required for create / create_subflow)'
@@ -1359,8 +538,6 @@ export const toolDefinition: MCPToolDefinition = {
1359
538
  description: 'Activate flow after creation (default: true)',
1360
539
  default: true
1361
540
  },
1362
-
1363
- // ── list params ──
1364
541
  type: {
1365
542
  type: 'string',
1366
543
  enum: ['flow', 'subflow', 'all'],
@@ -1377,8 +554,20 @@ export const toolDefinition: MCPToolDefinition = {
1377
554
  description: 'Max results for list',
1378
555
  default: 50
1379
556
  },
1380
-
1381
- // ── update params ──
557
+ action_type: {
558
+ type: 'string',
559
+ enum: ['log', 'create_record', 'update_record', 'notification', 'script', 'field_update', 'wait', 'approval'],
560
+ description: 'Action type to add (for add_action)',
561
+ default: 'log'
562
+ },
563
+ action_name: {
564
+ type: 'string',
565
+ description: 'Display name for the action (for add_action)'
566
+ },
567
+ action_inputs: {
568
+ type: 'object',
569
+ description: 'Key-value pairs for action inputs (e.g. {log_message: "test", log_level: "info"})'
570
+ },
1382
571
  update_fields: {
1383
572
  type: 'object',
1384
573
  description: 'Fields to update (for update action)',
@@ -1405,7 +594,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
1405
594
  switch (action) {
1406
595
 
1407
596
  // ────────────────────────────────────────────────────────────────
1408
- // CREATE (Scripted REST API → Table API fallback)
597
+ // CREATE
1409
598
  // ────────────────────────────────────────────────────────────────
1410
599
  case 'create':
1411
600
  case 'create_subflow': {
@@ -1468,7 +657,7 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
1468
657
  delete flowDefinition.trigger;
1469
658
  }
1470
659
 
1471
- // ── Pipeline: Scheduled Job (primary) → Table API (fallback) ──
660
+ // ── Pipeline: ProcessFlow API (primary) → Table API (fallback) ──
1472
661
  var flowSysId: string | null = null;
1473
662
  var usedMethod = 'table_api';
1474
663
  var versionCreated = false;
@@ -1477,82 +666,86 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
1477
666
  var actionsCreated = 0;
1478
667
  var varsCreated = 0;
1479
668
 
1480
- // Diagnostics: track every step for debugging "flow cannot be found" issues
669
+ // Diagnostics
1481
670
  var diagnostics: any = {
671
+ processflow_api: null,
1482
672
  table_api_used: false,
1483
673
  version_created: false,
1484
674
  version_method: null,
1485
675
  post_verify: null
1486
676
  };
1487
677
 
1488
- // ── Scheduled Job (primary — server-side GlideRecord) ───────
1489
- // This runs inside ServiceNow as a system job, so it CAN set
1490
- // computed fields like latest_version that Table API cannot.
1491
- // Also attempts sn_fd engine registration server-side.
678
+ // ── ProcessFlow API (primary — same REST endpoint as Flow Designer UI) ──
679
+ // Uses /api/now/processflow/flow to create engine-registered flows,
680
+ // then /api/now/processflow/versioning/create_version for versioning.
1492
681
  try {
1493
- var scheduledResult = await createFlowViaScheduledJob(client, {
682
+ var pfResult = await createFlowViaProcessFlowAPI(client, {
1494
683
  name: flowName,
1495
684
  description: flowDescription,
1496
- internalName: sanitizeInternalName(flowName),
1497
685
  isSubflow: isSubflow,
1498
- category: flowCategory,
1499
686
  runAs: flowRunAs,
1500
- shouldActivate: shouldActivate,
1501
- triggerType: triggerType,
1502
- triggerTable: flowTable,
1503
- triggerCondition: triggerCondition,
1504
- activities: activitiesArg.map(function (act: any, idx: number) {
1505
- return { name: act.name, type: act.type || 'script', inputs: act.inputs || {} };
1506
- }),
1507
- inputs: inputsArg,
1508
- outputs: outputsArg,
1509
- flowDefinition: flowDefinition
687
+ shouldActivate: shouldActivate
1510
688
  });
1511
- diagnostics.scheduled_job = {
1512
- success: scheduledResult.success,
1513
- tierUsed: scheduledResult.tierUsed,
1514
- latestVersionSet: scheduledResult.latestVersionSet,
1515
- latestVersionValue: scheduledResult.latestVersionValue,
1516
- steps: scheduledResult.steps,
1517
- error: scheduledResult.error
689
+ diagnostics.processflow_api = {
690
+ success: pfResult.success,
691
+ versionCreated: pfResult.versionCreated,
692
+ error: pfResult.error
1518
693
  };
1519
- if (scheduledResult.success && scheduledResult.flowSysId) {
1520
- flowSysId = scheduledResult.flowSysId;
1521
- usedMethod = 'scheduled_job (' + (scheduledResult.tierUsed || 'unknown') + ')';
1522
- versionCreated = !!scheduledResult.versionSysId;
694
+ if (pfResult.success && pfResult.flowSysId) {
695
+ flowSysId = pfResult.flowSysId;
696
+ usedMethod = 'processflow_api';
697
+ versionCreated = !!pfResult.versionCreated;
1523
698
  diagnostics.version_created = versionCreated;
1524
- diagnostics.version_method = 'scheduled_job';
1525
- diagnostics.latest_version_auto_set = scheduledResult.latestVersionSet;
1526
- // Extract engine registration results from scheduled job
1527
- if (scheduledResult.steps?.engine) {
1528
- diagnostics.engine_registration = scheduledResult.steps.engine;
1529
- }
1530
- // Extract trigger/action/variable results from scheduled job
1531
- if (scheduledResult.steps?.trigger) {
1532
- triggerCreated = !!scheduledResult.steps.trigger.success;
1533
- if (!scheduledResult.steps.trigger.success && scheduledResult.steps.trigger.error) {
1534
- factoryWarnings.push('Trigger: ' + scheduledResult.steps.trigger.error);
1535
- }
1536
- }
1537
- if (scheduledResult.steps?.actions) {
1538
- actionsCreated = scheduledResult.steps.actions.created || 0;
699
+ diagnostics.version_method = 'processflow_api';
700
+ }
701
+ } catch (pfErr: any) {
702
+ diagnostics.processflow_api = { error: pfErr.message || 'unknown' };
703
+ factoryWarnings.push('ProcessFlow API failed: ' + (pfErr.message || pfErr));
704
+ }
705
+
706
+ // ── Triggers, actions, variables for ProcessFlow-created flows ──
707
+ // Uses GraphQL mutations (same as Flow Designer UI) for proper engine registration
708
+ if (flowSysId && usedMethod === 'processflow_api') {
709
+ if (!isSubflow && triggerType !== 'manual') {
710
+ try {
711
+ var pfTrigResult = await addTriggerViaGraphQL(client, flowSysId, triggerType, flowTable, triggerCondition);
712
+ triggerCreated = pfTrigResult.success;
713
+ diagnostics.trigger_graphql = pfTrigResult;
714
+ } catch (_) { /* best-effort */ }
715
+ }
716
+ for (var pfai = 0; pfai < activitiesArg.length; pfai++) {
717
+ try {
718
+ var pfAct = activitiesArg[pfai];
719
+ var pfActResult = await addActionViaGraphQL(client, flowSysId, pfAct.type || 'log', pfAct.name || ('Action ' + (pfai + 1)), pfAct.inputs, pfai + 1);
720
+ if (pfActResult.success) actionsCreated++;
721
+ diagnostics['action_' + pfai] = pfActResult;
722
+ } catch (_) { /* best-effort */ }
723
+ }
724
+ if (isSubflow) {
725
+ for (var pfvi = 0; pfvi < inputsArg.length; pfvi++) {
726
+ try {
727
+ var pfInp = inputsArg[pfvi];
728
+ await client.post('/api/now/table/sys_hub_flow_variable', {
729
+ flow: flowSysId, name: pfInp.name, label: pfInp.label || pfInp.name,
730
+ type: pfInp.type || 'string', mandatory: pfInp.mandatory || false,
731
+ default_value: pfInp.default_value || '', variable_type: 'input'
732
+ });
733
+ varsCreated++;
734
+ } catch (_) {}
1539
735
  }
1540
- if (scheduledResult.steps?.variables) {
1541
- varsCreated = scheduledResult.steps.variables.created || 0;
736
+ for (var pfvo = 0; pfvo < outputsArg.length; pfvo++) {
737
+ try {
738
+ var pfOut = outputsArg[pfvo];
739
+ await client.post('/api/now/table/sys_hub_flow_variable', {
740
+ flow: flowSysId, name: pfOut.name, label: pfOut.label || pfOut.name,
741
+ type: pfOut.type || 'string', variable_type: 'output'
742
+ });
743
+ varsCreated++;
744
+ } catch (_) {}
1542
745
  }
1543
746
  }
1544
- } catch (schedErr: any) {
1545
- diagnostics.scheduled_job = { error: schedErr.message || 'unknown' };
1546
- factoryWarnings.push('Scheduled job failed: ' + (schedErr.message || schedErr));
1547
747
  }
1548
748
 
1549
- // BR compile + engine REST registration skipped:
1550
- // - BR: FlowDesigner unavailable in all contexts, FlowAPI.compile returns error,
1551
- // all Script Includes (FlowDesignerScriptable etc.) don't exist on PDI instances
1552
- // - REST: all sn_fd endpoints return 400 "Requested URI does not represent any resource"
1553
- // Both add ~20s combined and provide zero value. Flow records are functional
1554
- // (listable, queryable, deletable) but cannot be opened in Flow Designer UI.
1555
-
1556
749
  // ── Table API fallback (last resort) ─────────────────────────
1557
750
  if (!flowSysId) {
1558
751
  diagnostics.table_api_used = true;
@@ -1626,91 +819,24 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
1626
819
  factoryWarnings.push('sys_hub_flow_version creation failed: ' + (verError.message || verError));
1627
820
  }
1628
821
 
1629
- // Create trigger instance (non-manual flows only)
1630
- // Use precise prefix search, then fallback to creating without action_type
822
+ // Create trigger via GraphQL (same method as Flow Designer UI)
1631
823
  if (!isSubflow && triggerType !== 'manual') {
1632
824
  try {
1633
- var triggerDefId: string | null = null;
1634
-
1635
- // Search 1: exact sn_fd.trigger.* prefix
1636
- var exactTrigNames: Record<string, string[]> = {
1637
- 'record_created': ['sn_fd.trigger.record_created', 'global.sn_fd.trigger.record_created'],
1638
- 'record_updated': ['sn_fd.trigger.record_updated', 'global.sn_fd.trigger.record_updated'],
1639
- 'scheduled': ['sn_fd.trigger.scheduled', 'global.sn_fd.trigger.scheduled']
1640
- };
1641
- var exactCands = exactTrigNames[triggerType] || [];
1642
- for (var eci = 0; eci < exactCands.length && !triggerDefId; eci++) {
1643
- var exactResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
1644
- params: {
1645
- sysparm_query: 'internal_name=' + exactCands[eci],
1646
- sysparm_fields: 'sys_id',
1647
- sysparm_limit: 1
1648
- }
1649
- });
1650
- triggerDefId = exactResp.data.result?.[0]?.sys_id || null;
1651
- }
1652
-
1653
- // Search 2: STARTSWITH sn_fd.trigger
1654
- if (!triggerDefId) {
1655
- var prefixResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
1656
- params: {
1657
- sysparm_query: 'internal_nameSTARTSWITHsn_fd.trigger',
1658
- sysparm_fields: 'sys_id,internal_name',
1659
- sysparm_limit: 10
1660
- }
1661
- });
1662
- var prefixResults = prefixResp.data.result || [];
1663
- for (var pri = 0; pri < prefixResults.length && !triggerDefId; pri++) {
1664
- if ((prefixResults[pri].internal_name || '').indexOf(triggerType.replace('record_', '')) > -1) {
1665
- triggerDefId = prefixResults[pri].sys_id;
1666
- }
1667
- }
1668
- }
1669
-
1670
- // Create trigger instance (with or without action_type)
1671
- var triggerData: any = {
1672
- flow: flowSysId,
1673
- name: triggerType,
1674
- order: 0,
1675
- active: true
1676
- };
1677
- if (triggerDefId) triggerData.action_type = triggerDefId;
1678
- if (flowTable) triggerData.table = flowTable;
1679
- if (triggerCondition) triggerData.condition = triggerCondition;
1680
-
1681
- await client.post('/api/now/table/sys_hub_trigger_instance', triggerData);
1682
- triggerCreated = true;
825
+ var taTrigResult = await addTriggerViaGraphQL(client, flowSysId, triggerType, flowTable, triggerCondition);
826
+ triggerCreated = taTrigResult.success;
827
+ diagnostics.trigger_graphql = taTrigResult;
1683
828
  } catch (triggerError) {
1684
829
  // Best-effort
1685
830
  }
1686
831
  }
1687
832
 
1688
- // Create action instances
833
+ // Create actions via GraphQL (same method as Flow Designer UI)
1689
834
  for (var ai = 0; ai < activitiesArg.length; ai++) {
1690
835
  var activity = activitiesArg[ai];
1691
836
  try {
1692
- var actionTypeName = activity.type || 'script';
1693
- var actionTypeQuery = 'internal_nameLIKE' + actionTypeName + '^ORnameLIKE' + actionTypeName;
1694
-
1695
- var actionDefResp = await client.get('/api/now/table/sys_hub_action_type_definition', {
1696
- params: {
1697
- sysparm_query: actionTypeQuery,
1698
- sysparm_fields: 'sys_id,name,internal_name',
1699
- sysparm_limit: 1
1700
- }
1701
- });
1702
-
1703
- var actionDefId = actionDefResp.data.result?.[0]?.sys_id;
1704
- var instanceData: any = {
1705
- flow: flowSysId,
1706
- name: activity.name,
1707
- order: (ai + 1) * 100,
1708
- active: true
1709
- };
1710
- if (actionDefId) instanceData.action_type = actionDefId;
1711
-
1712
- await client.post('/api/now/table/sys_hub_action_instance', instanceData);
1713
- actionsCreated++;
837
+ var taActResult = await addActionViaGraphQL(client, flowSysId, activity.type || 'log', activity.name || ('Action ' + (ai + 1)), activity.inputs, ai + 1);
838
+ if (taActResult.success) actionsCreated++;
839
+ diagnostics['action_' + ai] = taActResult;
1714
840
  } catch (actError) {
1715
841
  // Best-effort
1716
842
  }
@@ -1815,11 +941,9 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
1815
941
  }
1816
942
 
1817
943
  // ── Build summary ───────────────────────────────────────────
1818
- var methodLabel = usedMethod.startsWith('scheduled_job')
1819
- ? 'Scheduled Job (server-side GlideRecord)'
1820
- : usedMethod === 'scripted_rest_api'
1821
- ? 'Scripted REST API (GlideRecord)'
1822
- : 'Table API' + (factoryWarnings.length > 0 ? ' (fallback)' : '');
944
+ var methodLabel = usedMethod === 'processflow_api'
945
+ ? 'ProcessFlow API (Flow Designer engine)'
946
+ : 'Table API' + (factoryWarnings.length > 0 ? ' (fallback)' : '');
1823
947
 
1824
948
  var createSummary = summary()
1825
949
  .success('Created ' + (isSubflow ? 'subflow' : 'flow') + ': ' + flowName)
@@ -1855,17 +979,13 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
1855
979
 
1856
980
  // Diagnostics section
1857
981
  createSummary.blank().line('Diagnostics:');
1858
- if (diagnostics.scheduled_job) {
1859
- var sj = diagnostics.scheduled_job;
1860
- createSummary.indented('Scheduled job: ' + (sj.success ? 'success' : 'failed') + (sj.tierUsed ? ' (' + sj.tierUsed + ')' : ''));
1861
- if (sj.error) createSummary.indented(' Error: ' + sj.error);
982
+ if (diagnostics.processflow_api) {
983
+ var pf = diagnostics.processflow_api;
984
+ createSummary.indented('ProcessFlow API: ' + (pf.success ? 'success' : 'failed') + (pf.versionCreated ? ' (version created)' : ''));
985
+ if (pf.error) createSummary.indented(' Error: ' + pf.error);
1862
986
  }
1863
- createSummary.indented('Table API used: ' + diagnostics.table_api_used);
987
+ createSummary.indented('Table API fallback used: ' + diagnostics.table_api_used);
1864
988
  createSummary.indented('Version created: ' + diagnostics.version_created + (diagnostics.version_method ? ' (' + diagnostics.version_method + ')' : ''));
1865
- createSummary.indented('Engine compile: skipped (not available on this instance — FlowDesigner unavailable, REST 400s, no Script Includes)');
1866
- if (diagnostics.latest_version_auto_set !== undefined) {
1867
- createSummary.indented('latest_version: ' + (diagnostics.latest_version_auto_set ? 'set' : 'null'));
1868
- }
1869
989
  if (diagnostics.post_verify) {
1870
990
  if (diagnostics.post_verify.error) {
1871
991
  createSummary.indented('Post-verify: error — ' + diagnostics.post_verify.error);
@@ -2231,6 +1351,68 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
2231
1351
  }, {}, deleteSummary.build());
2232
1352
  }
2233
1353
 
1354
+ // ────────────────────────────────────────────────────────────────
1355
+ // ADD_TRIGGER
1356
+ // ────────────────────────────────────────────────────────────────
1357
+ case 'add_trigger': {
1358
+ if (!args.flow_id) {
1359
+ throw new SnowFlowError(ErrorType.VALIDATION_ERROR, 'flow_id is required for add_trigger');
1360
+ }
1361
+ var addTrigFlowId = await resolveFlowId(client, args.flow_id);
1362
+ var addTrigType = args.trigger_type || 'record_create_or_update';
1363
+ var addTrigTable = args.table || '';
1364
+ var addTrigCondition = args.trigger_condition || '';
1365
+
1366
+ var addTrigResult = await addTriggerViaGraphQL(client, addTrigFlowId, addTrigType, addTrigTable, addTrigCondition);
1367
+
1368
+ var addTrigSummary = summary();
1369
+ if (addTrigResult.success) {
1370
+ addTrigSummary
1371
+ .success('Trigger added via GraphQL')
1372
+ .field('Flow', addTrigFlowId)
1373
+ .field('Type', addTrigType)
1374
+ .field('Trigger ID', addTrigResult.triggerId || 'unknown');
1375
+ if (addTrigTable) addTrigSummary.field('Table', addTrigTable);
1376
+ } else {
1377
+ addTrigSummary.error('Failed to add trigger: ' + (addTrigResult.error || 'unknown'));
1378
+ }
1379
+
1380
+ return addTrigResult.success
1381
+ ? createSuccessResult({ action: 'add_trigger', ...addTrigResult }, {}, addTrigSummary.build())
1382
+ : createErrorResult(addTrigResult.error || 'Failed to add trigger');
1383
+ }
1384
+
1385
+ // ────────────────────────────────────────────────────────────────
1386
+ // ADD_ACTION
1387
+ // ────────────────────────────────────────────────────────────────
1388
+ case 'add_action': {
1389
+ if (!args.flow_id) {
1390
+ throw new SnowFlowError(ErrorType.VALIDATION_ERROR, 'flow_id is required for add_action');
1391
+ }
1392
+ var addActFlowId = await resolveFlowId(client, args.flow_id);
1393
+ var addActType = args.action_type || 'log';
1394
+ var addActName = args.action_name || args.name || addActType;
1395
+ var addActInputs = args.action_inputs || args.inputs || {};
1396
+
1397
+ var addActResult = await addActionViaGraphQL(client, addActFlowId, addActType, addActName, addActInputs);
1398
+
1399
+ var addActSummary = summary();
1400
+ if (addActResult.success) {
1401
+ addActSummary
1402
+ .success('Action added via GraphQL')
1403
+ .field('Flow', addActFlowId)
1404
+ .field('Type', addActType)
1405
+ .field('Name', addActName)
1406
+ .field('Action ID', addActResult.actionId || 'unknown');
1407
+ } else {
1408
+ addActSummary.error('Failed to add action: ' + (addActResult.error || 'unknown'));
1409
+ }
1410
+
1411
+ return addActResult.success
1412
+ ? createSuccessResult({ action: 'add_action', ...addActResult }, {}, addActSummary.build())
1413
+ : createErrorResult(addActResult.error || 'Failed to add action');
1414
+ }
1415
+
2234
1416
  default:
2235
1417
  throw new SnowFlowError(ErrorType.VALIDATION_ERROR, 'Unknown action: ' + action);
2236
1418
  }
@@ -2251,5 +1433,5 @@ export async function execute(args: any, context: ServiceNowContext): Promise<To
2251
1433
  }
2252
1434
  }
2253
1435
 
2254
- export const version = '5.0.0';
1436
+ export const version = '6.0.0';
2255
1437
  export const author = 'Snow-Flow Team';