@servicenow/sdk-build-plugins 4.8.0 → 4.8.1

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.
Files changed (45) hide show
  1. package/dist/flow/flow-logic/flow-logic-diagnostics.js +2 -1
  2. package/dist/flow/flow-logic/flow-logic-diagnostics.js.map +1 -1
  3. package/dist/flow/flow-logic/flow-logic-plugin.js.map +1 -1
  4. package/dist/flow/plugins/flow-action-definition-plugin.js +81 -16
  5. package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -1
  6. package/dist/flow/plugins/flow-definition-plugin.js +70 -7
  7. package/dist/flow/plugins/flow-definition-plugin.js.map +1 -1
  8. package/dist/flow/plugins/flow-instance-plugin.d.ts +35 -1
  9. package/dist/flow/plugins/flow-instance-plugin.js +240 -6
  10. package/dist/flow/plugins/flow-instance-plugin.js.map +1 -1
  11. package/dist/flow/plugins/step-instance-plugin.js +60 -0
  12. package/dist/flow/plugins/step-instance-plugin.js.map +1 -1
  13. package/dist/flow/post-install.d.ts +2 -1
  14. package/dist/flow/post-install.js +31 -4
  15. package/dist/flow/post-install.js.map +1 -1
  16. package/dist/flow/utils/complex-object-resolver.js +4 -2
  17. package/dist/flow/utils/complex-object-resolver.js.map +1 -1
  18. package/dist/flow/utils/datapill-transformer.d.ts +5 -72
  19. package/dist/flow/utils/datapill-transformer.js +199 -28
  20. package/dist/flow/utils/datapill-transformer.js.map +1 -1
  21. package/dist/flow/utils/flow-io-to-record.js +24 -15
  22. package/dist/flow/utils/flow-io-to-record.js.map +1 -1
  23. package/dist/flow/utils/flow-shapes.d.ts +7 -1
  24. package/dist/flow/utils/flow-shapes.js +19 -0
  25. package/dist/flow/utils/flow-shapes.js.map +1 -1
  26. package/dist/flow/utils/flow-variable-processor.d.ts +6 -6
  27. package/dist/flow/utils/flow-variable-processor.js +8 -8
  28. package/dist/flow/utils/flow-variable-processor.js.map +1 -1
  29. package/dist/index.d.ts +1 -1
  30. package/dist/index.js +2 -1
  31. package/dist/index.js.map +1 -1
  32. package/package.json +5 -5
  33. package/src/flow/flow-logic/flow-logic-diagnostics.ts +2 -1
  34. package/src/flow/flow-logic/flow-logic-plugin.ts +0 -1
  35. package/src/flow/plugins/flow-action-definition-plugin.ts +92 -25
  36. package/src/flow/plugins/flow-definition-plugin.ts +114 -8
  37. package/src/flow/plugins/flow-instance-plugin.ts +262 -6
  38. package/src/flow/plugins/step-instance-plugin.ts +73 -1
  39. package/src/flow/post-install.ts +36 -5
  40. package/src/flow/utils/complex-object-resolver.ts +4 -2
  41. package/src/flow/utils/datapill-transformer.ts +248 -36
  42. package/src/flow/utils/flow-io-to-record.ts +28 -14
  43. package/src/flow/utils/flow-shapes.ts +19 -0
  44. package/src/flow/utils/flow-variable-processor.ts +21 -10
  45. package/src/index.ts +1 -1
@@ -82,7 +82,12 @@ import { isDataPill } from '../utils/complex-object-resolver'
82
82
  import { wrapWithDataPillCall, extractDataPillNames } from '../utils/pill-shape-helpers'
83
83
  import { ArrayShape } from '@servicenow/sdk-build-core'
84
84
  import { processFlowInputs, processFlowOutputs, processFlowVariables } from '../utils/flow-variable-processor'
85
- import { buildUuidToIdentifierMap, processInstanceForDatapills, uuidToRecordMap } from '../utils/datapill-transformer'
85
+ import {
86
+ buildUuidToIdentifierMap,
87
+ detectHoistedOutputBindings,
88
+ processInstanceForDatapills,
89
+ uuidToRecordMap,
90
+ } from '../utils/datapill-transformer'
86
91
  import {
87
92
  DEFAULT_PARAM_NAME,
88
93
  PARALLEL_ORDER_SEPARATOR,
@@ -646,7 +651,6 @@ function extractAllPills(
646
651
  })
647
652
  }
648
653
  } else {
649
- // NOT a forEach parameter - preserve full property path
650
654
  expression = new PropertyAccessShape({
651
655
  source,
652
656
  elements: [identifier, property],
@@ -774,6 +778,7 @@ function transformDataPill(
774
778
  // {{flowVariables}} and {{outputs}} should remain as bare PropertyAccessShape without wrapping
775
779
  if (stringValue === '{{flowVariables}}' || stringValue === '{{outputs}}') {
776
780
  const allPills = extractAllPills(stringValue, uuidToIdentifierMap, source, diagnostics, parameterName)
781
+
777
782
  if (allPills.length === 1 && allPills[0]?.expression.is(PropertyAccessShape)) {
778
783
  return allPills[0].expression as PropertyAccessShape
779
784
  }
@@ -1497,7 +1502,7 @@ function postProcessAssignSubflowOutputs(
1497
1502
  subflowDefinitionRecord: Record,
1498
1503
  logger: Logger
1499
1504
  ): ValuesJSON {
1500
- if (!valuesJSON || !valuesJSON.outputsToAssign || !Array.isArray(valuesJSON.outputsToAssign)) {
1505
+ if (!valuesJSON?.outputsToAssign || !Array.isArray(valuesJSON.outputsToAssign)) {
1501
1506
  return valuesJSON
1502
1507
  }
1503
1508
 
@@ -1682,6 +1687,8 @@ export const FlowDefinitionPlugin = Plugin.create({
1682
1687
  return deleteMultipleDiff(existing, incoming, descendants, context)
1683
1688
  },
1684
1689
  async toShape(record, { descendants, diagnostics, transform, database, logger }) {
1690
+ const masterSnapshotId = record.get('master_snapshot')?.ifString()?.getValue()
1691
+
1685
1692
  let type = record.get('type')?.getValue()
1686
1693
  if (!type) {
1687
1694
  logger.warn(`sys_hub_flow record has no type specified, defaulting to 'flow'`)
@@ -1771,6 +1778,15 @@ export const FlowDefinitionPlugin = Plugin.create({
1771
1778
  // Use recordToShapeMap to access ui_id from records (datapills reference ui_id, not $id)
1772
1779
  const uuidToIdentifierMap = buildUuidToIdentifierMap(recordToShapeMap)
1773
1780
 
1781
+ // Hoisting guard: if any action nested inside a DoInParallel or TryCatch block has its output
1782
+ // referenced from outside that block, fall back to Record() API.
1783
+ if (detectHoistedOutputBindings(allInstances)) {
1784
+ logger.warn(
1785
+ `Hoisting detected: action instance outputs are referenced outside their DoInParallel/TryCatch scope, falling back to Record() api. Flow = ${record.getId().getValue()}`
1786
+ )
1787
+ return { success: false }
1788
+ }
1789
+
1774
1790
  // For the flow body, only use top-level instances (those without parent_ui_id). Also exclude Catch
1775
1791
  // records — they are subsumed into their paired Try's `wfa.flowLogic.tryCatch(...)` shape via
1776
1792
  // `connected_to`, and emitting them separately would produce a duplicate top-level statement.
@@ -1830,7 +1846,6 @@ export const FlowDefinitionPlugin = Plugin.create({
1830
1846
  // 2. Semantic pills: {{trigger.property}}, {{flow_variable.name}} → references to flow parameters (e.g., {{trigger.table_name}} → params.trigger.table_name)
1831
1847
 
1832
1848
  // Process all instances (Action, Subflow, and Flow Logic) recursively
1833
- // This handles datapill transformation for all instance types and their nested children
1834
1849
  instanceShapes.forEach((instanceShape: Shape, index) => {
1835
1850
  const result = processInstanceForDatapills(
1836
1851
  instanceShape,
@@ -1848,6 +1863,7 @@ export const FlowDefinitionPlugin = Plugin.create({
1848
1863
  instanceShapes[index] = result.shape
1849
1864
  }
1850
1865
  })
1866
+
1851
1867
  const flowBody = new ArrowFunctionShape({
1852
1868
  source: record,
1853
1869
  parameters: [new IdentifierShape({ source: record, name: paramName })],
@@ -1862,6 +1878,7 @@ export const FlowDefinitionPlugin = Plugin.create({
1862
1878
  record.transform(({ $ }) => ({
1863
1879
  $id: $.val(NowIdShape.from(record)),
1864
1880
  name: $,
1881
+ internalName: $.from('internal_name'),
1865
1882
  description: $.def(EMPTY_STRING),
1866
1883
  protectionPolicy: $.from('sys_policy').def(EMPTY_STRING),
1867
1884
  runAs: $.from('run_as').def(RUN_AS_DEFAULT),
@@ -1877,6 +1894,7 @@ export const FlowDefinitionPlugin = Plugin.create({
1877
1894
  flowPriority: $.from('flow_priority')
1878
1895
  .map((p) => p.ifString()?.getValue() || FLOW_PRIORITY_DEFAULT)
1879
1896
  .def(FLOW_PRIORITY_DEFAULT),
1897
+ ...(masterSnapshotId ? { masterSnapshot: $.val(masterSnapshotId) } : {}),
1880
1898
  ...(isSubflow
1881
1899
  ? {
1882
1900
  annotation: $.def(EMPTY_STRING),
@@ -1975,12 +1993,14 @@ export const FlowDefinitionPlugin = Plugin.create({
1975
1993
  }
1976
1994
 
1977
1995
  const roles = flowConfiguration.get('runWithRoles').ifArray()?.getElements() ?? []
1996
+ const internalName = flowConfiguration.get('internalName').ifString()?.getValue()
1978
1997
  const flowDefinitionRecord = await factory.createRecord({
1979
1998
  source: callExpression,
1980
1999
  table: 'sys_hub_flow',
1981
2000
  explicitId: flowConfiguration.get('$id'),
1982
2001
  properties: flowConfiguration.transform(({ $ }) => ({
1983
2002
  name: $,
2003
+ ...(internalName ? { internal_name: $.val(internalName) } : {}),
1984
2004
  annotation: $.def(EMPTY_STRING),
1985
2005
  description: $.def(EMPTY_STRING),
1986
2006
  access: $.def(ACCESS_DEFAULT),
@@ -2046,9 +2066,11 @@ export const FlowDefinitionPlugin = Plugin.create({
2046
2066
  relatedRecords.push(...flowVarRecords)
2047
2067
  }
2048
2068
 
2069
+ const inputsConfig = isSubflow ? flowConfiguration.get('inputs').ifObject()?.asObject() : undefined
2070
+ const outputsConfig = isSubflow ? flowConfiguration.get('outputs').ifObject()?.asObject() : undefined
2071
+
2049
2072
  if (isSubflow) {
2050
2073
  // Process subflow inputs and outputs
2051
- const inputsConfig = flowConfiguration.get('inputs').ifObject()?.asObject()
2052
2074
  const inputRecords = await processFlowInputs(
2053
2075
  inputsConfig,
2054
2076
  flowDefinitionRecord,
@@ -2057,7 +2079,6 @@ export const FlowDefinitionPlugin = Plugin.create({
2057
2079
  )
2058
2080
  relatedRecords.push(...inputRecords)
2059
2081
 
2060
- const outputsConfig = flowConfiguration.get('outputs').ifObject()?.asObject()
2061
2082
  const outputRecords = await processFlowOutputs(
2062
2083
  outputsConfig,
2063
2084
  flowDefinitionRecord,
@@ -2260,16 +2281,101 @@ export const FlowDefinitionPlugin = Plugin.create({
2260
2281
  )
2261
2282
 
2262
2283
  const labelCacheData = Array.from(labelCacheMap.values())
2284
+ const labelCacheValue = labelCacheData.length > 0 ? JSON.stringify(labelCacheData) : '[]'
2285
+
2263
2286
  const flowDefWithMetadata =
2264
2287
  labelCacheData.length > 0
2265
2288
  ? flowDefinitionRecord.merge({
2266
- label_cache: JSON.stringify(labelCacheData),
2289
+ label_cache: labelCacheValue,
2267
2290
  })
2268
2291
  : flowDefinitionRecord
2269
2292
 
2293
+ const masterSnapshotFromConfig = flowConfiguration.get('masterSnapshot')?.ifString()?.getValue()
2294
+
2295
+ if (!masterSnapshotFromConfig) {
2296
+ return {
2297
+ success: true,
2298
+ value: flowDefWithMetadata.with(...relatedRecords),
2299
+ }
2300
+ }
2301
+
2302
+ const snapshotRecord = await factory.createRecord({
2303
+ source: callExpression,
2304
+ table: 'sys_hub_flow_snapshot',
2305
+ explicitId: masterSnapshotFromConfig,
2306
+ properties: flowConfiguration.transform(({ $ }) => ({
2307
+ access: $.def(ACCESS_DEFAULT),
2308
+ active: $.val(false),
2309
+ allow_high_security_roles: $.def(BOOLEAN_FALSE_STRING),
2310
+ annotation: $.def(EMPTY_STRING),
2311
+ attributes: $.def(EMPTY_STRING),
2312
+ callable_by_client_api: $.def(BOOLEAN_FALSE_STRING),
2313
+ category: $.def(EMPTY_STRING),
2314
+ description: $.def(EMPTY_STRING),
2315
+ flow_priority: $.from('flowPriority').def(FLOW_PRIORITY_DEFAULT),
2316
+ label_cache: $.val(labelCacheValue),
2317
+ master: $.val(true),
2318
+ name: $,
2319
+ natlang: $.def(EMPTY_STRING),
2320
+ outputs: $.def(EMPTY_STRING),
2321
+ parent_flow: $.val(flowDefinitionRecord.getId().getValue()),
2322
+ remote_trigger_id: $.def(EMPTY_STRING),
2323
+ run_as: $.from('runAs').def(RUN_AS_DEFAULT),
2324
+ run_with_roles: $.val(
2325
+ Array.from(
2326
+ new Set(roles.map((r) => r.ifRecord()?.getId().getValue() ?? r.getValue()))
2327
+ ).join(',')
2328
+ ),
2329
+ sc_callable: $.def(BOOLEAN_FALSE_STRING),
2330
+ status: $.val(FLOW_STATUS_DRAFT),
2331
+ sys_policy: $.from('protectionPolicy').def(EMPTY_STRING),
2332
+ type: isFlow ? $.def(FLOW_TYPE_FLOW) : $.def(FLOW_TYPE_SUBFLOW),
2333
+ version: $.def(FLOW_VERSION),
2334
+ })),
2335
+ })
2336
+
2337
+ const snapshotRelatedRecords: Record[] = []
2338
+
2339
+ if (isSubflow) {
2340
+ const snapshotInputRecords = await processFlowInputs(
2341
+ inputsConfig,
2342
+ snapshotRecord,
2343
+ factory,
2344
+ diagnostics,
2345
+ 'sys_hub_flow_snapshot'
2346
+ )
2347
+ snapshotRelatedRecords.push(...snapshotInputRecords)
2348
+
2349
+ const snapshotOutputRecords = await processFlowOutputs(
2350
+ outputsConfig,
2351
+ snapshotRecord,
2352
+ factory,
2353
+ diagnostics,
2354
+ 'sys_hub_flow_snapshot'
2355
+ )
2356
+ snapshotRelatedRecords.push(...snapshotOutputRecords)
2357
+ }
2358
+
2359
+ if (flowConfiguration.has('flowVariables')) {
2360
+ const snapshotVarRecords = await processFlowVariables(
2361
+ flowConfiguration.get('flowVariables').asObject(),
2362
+ snapshotRecord,
2363
+ factory,
2364
+ diagnostics,
2365
+ 'sys_hub_flow_snapshot'
2366
+ )
2367
+ snapshotRelatedRecords.push(...snapshotVarRecords)
2368
+ }
2369
+
2370
+ const snapshotId = snapshotRecord.getId().getValue()
2371
+ const finalFlowRecord = flowDefWithMetadata.merge({
2372
+ latest_snapshot: snapshotId,
2373
+ master_snapshot: snapshotId,
2374
+ })
2375
+
2270
2376
  return {
2271
2377
  success: true,
2272
- value: flowDefWithMetadata.with(...relatedRecords),
2378
+ value: finalFlowRecord.with(...relatedRecords, snapshotRecord, ...snapshotRelatedRecords),
2273
2379
  }
2274
2380
  },
2275
2381
  },
@@ -45,6 +45,7 @@ import {
45
45
  ActionSubflowInstanceShape,
46
46
  ApprovalDueDateShape,
47
47
  ApprovalRulesShape,
48
+ getRefSysId,
48
49
  InlineScriptShape,
49
50
  } from '../utils/flow-shapes'
50
51
  import { FDInlineScriptCallShape } from './inline-script-plugin'
@@ -142,6 +143,119 @@ function parseComplexObjectValues(obj: unknown): unknown {
142
143
  return parseValue(obj)
143
144
  }
144
145
 
146
+ /**
147
+ * Returns the complex-object payload type ('object' | 'array.object') when the given value is a
148
+ * serialized FD complex-object JSON string (has both `complexObjectSchema` and `complexObject`).
149
+ * Returns `undefined` for any other value.
150
+ */
151
+ export function getComplexObjectPayloadType(value: unknown): 'object' | 'array.object' | undefined {
152
+ if (typeof value !== 'string') {
153
+ return undefined
154
+ }
155
+ try {
156
+ const parsed = JSON.parse(value)
157
+ if (parsed && typeof parsed === 'object' && 'complexObjectSchema' in parsed && 'complexObject' in parsed) {
158
+ const co = (parsed as { complexObject?: unknown }).complexObject
159
+ return co && typeof co === 'object' && '$COCollectionField' in co ? 'array.object' : 'object'
160
+ }
161
+ } catch {
162
+ // not JSON / not a complex-object payload
163
+ }
164
+ return undefined
165
+ }
166
+
167
+ /**
168
+ * Decodes a serialized FD complex-object payload (FlowObject or FlowArray) into a plain
169
+ * JavaScript object/array by extracting the `$cv.$v` leaf values from `complexObject`.
170
+ *
171
+ * Returns `undefined` if the string cannot be decoded (malformed, empty complexObject, etc.).
172
+ * Falls back gracefully so callers can keep the verbatim string for spoke/snapshot references.
173
+ *
174
+ * Exported only for unit testing.
175
+ */
176
+ type PlainObj = { [key: string]: unknown }
177
+
178
+ export function decodeComplexObjectPayload(jsonString: string): PlainObj | PlainObj[] | unknown[] | undefined {
179
+ try {
180
+ const parsed = JSON.parse(jsonString)
181
+ if (!parsed || typeof parsed !== 'object' || !('complexObject' in parsed)) {
182
+ return undefined
183
+ }
184
+ return decodeComplexObjectNode(parsed.complexObject as PlainObj)
185
+ } catch {
186
+ return undefined
187
+ }
188
+ }
189
+
190
+ function decodeComplexObjectNode(co: PlainObj): PlainObj | PlainObj[] | unknown[] {
191
+ if ('$COCollectionField' in co && Array.isArray(co['$COCollectionField'])) {
192
+ const arr = co['$COCollectionField'] as unknown[]
193
+ // Primitive arrays (e.g. FlowArray<StringColumn>, FlowArray<IntegerColumn>) store plain
194
+ // strings/numbers in $COCollectionField — not $cv-wrapped objects. Return them directly
195
+ // to avoid decodeComplexObjectItem treating string characters or number properties as fields.
196
+ if (arr.length === 0 || typeof arr[0] !== 'object' || arr[0] === null) {
197
+ return arr
198
+ }
199
+ return (arr as PlainObj[]).map(decodeComplexObjectItem)
200
+ }
201
+ return decodeComplexObjectItem(co)
202
+ }
203
+
204
+ function decodeComplexObjectItem(item: PlainObj): PlainObj {
205
+ const result: PlainObj = {}
206
+ for (const [key, val] of Object.entries(item)) {
207
+ if (key === 'name$' || key.includes('.$')) {
208
+ continue
209
+ }
210
+ if (Array.isArray(val)) {
211
+ // Nested FlowArray<primitive> (Array.String, Array.Integer, Array.Boolean, etc.)
212
+ // within a FlowObject: buildCv stores plain values directly as a bare array —
213
+ // no $COCollectionField wrapper for primitive element types.
214
+ // Must check Array.isArray BEFORE typeof object because arrays are objects in JS.
215
+ if (val.length === 0 || typeof val[0] !== 'object' || val[0] === null) {
216
+ result[key] = val
217
+ } else {
218
+ result[key] = (val as PlainObj[]).map(decodeComplexObjectItem)
219
+ }
220
+ } else if (typeof val === 'object' && val !== null) {
221
+ const v = val as PlainObj
222
+ if ('$cv' in v) {
223
+ // ServiceNow UI format: leaf value wrapped in { $cv: { $c, $v } }
224
+ const cv = v['$cv'] as PlainObj
225
+ const rawVal = cv?.['$v'] ?? ''
226
+ const javaClass = cv?.['$c'] as string | undefined
227
+ // Only coerce to number for known numeric Java types — avoids silently converting
228
+ // string choice values (e.g. state: '1' stored as java.lang.String) to numbers.
229
+ const isNumericType =
230
+ javaClass === 'java.lang.Integer' ||
231
+ javaClass === 'java.lang.Long' ||
232
+ javaClass === 'java.lang.Double' ||
233
+ javaClass === 'java.lang.Float'
234
+ result[key] =
235
+ isNumericType && typeof rawVal === 'string' && rawVal.trim() !== '' ? Number(rawVal) : rawVal
236
+ } else if ('$COCollectionField' in v) {
237
+ // Nested FlowArray<object> (Array.Object) within a FlowObject:
238
+ // buildCv wraps complex array elements in { $COCollectionField: [...] }.
239
+ const arr = v['$COCollectionField'] as unknown[]
240
+ if (arr.length === 0 || typeof arr[0] !== 'object' || arr[0] === null) {
241
+ result[key] = arr
242
+ } else {
243
+ result[key] = (arr as PlainObj[]).map(decodeComplexObjectItem)
244
+ }
245
+ } else {
246
+ // Nested FlowObject
247
+ result[key] = decodeComplexObjectItem(v)
248
+ }
249
+ } else if (val !== undefined) {
250
+ // Fluent build format: raw primitive stored directly (no $cv wrapping).
251
+ // buildArrayPayload skips buildCv for array elements, so string/number/boolean
252
+ // values appear as-is in $COCollectionField items.
253
+ result[key] = val
254
+ }
255
+ }
256
+ return result
257
+ }
258
+
145
259
  /**
146
260
  * Normalize raw input value based on uiType.
147
261
  * - Attempts to JSON.parse strings.
@@ -244,7 +358,13 @@ function buildInstanceToShape({
244
358
 
245
359
  const definitionInputs =
246
360
  instanceDef instanceof Record
247
- ? instanceDef.flat().filter((v) => v.getTable() === inputsTableName)
361
+ ? instanceDef
362
+ .flat()
363
+ .filter(
364
+ (v) =>
365
+ v.getTable() === inputsTableName &&
366
+ getRefSysId(v.get('model')) === instanceDef.getId().getValue()
367
+ )
248
368
  : undefined
249
369
 
250
370
  const zippedInputs = record.get(zippedColumn)?.ifString()?.getValue() ?? ''
@@ -586,6 +706,36 @@ function buildInlineScriptShapeFromXml(
586
706
  return undefined
587
707
  }
588
708
 
709
+ /**
710
+ * Reconstructs a TemplateValueShape with FDInlineScriptCallShape sub-fields when the
711
+ * V2 JSON payload carries scripted sub-fields (scriptActive=false at top level, with
712
+ * a `script` map keyed by sub-field name).
713
+ *
714
+ * Called during XML → Fluent transform to restore wfa.inlineScript() calls that were
715
+ * inside TemplateValue({...}) in the original Fluent source.
716
+ */
717
+ export function applyTemplateValueSubFieldScripts(
718
+ value: unknown,
719
+ script: { [key: string]: { scriptActive: boolean; script: string } } | undefined,
720
+ source: Record
721
+ ): unknown {
722
+ if (!script || !(value instanceof TemplateValueShape)) {
723
+ return value
724
+ }
725
+
726
+ const templateValue = value.getTemplateValue()
727
+ const newProps: globalThis.Record<string, unknown> = {}
728
+ for (const [field, fieldShape] of templateValue.entries({ resolve: false })) {
729
+ const fieldScript = script[field]
730
+ if (fieldScript?.scriptActive && fieldScript?.script) {
731
+ newProps[field] = new FDInlineScriptCallShape({ source, scriptContent: fieldScript.script })
732
+ } else {
733
+ newProps[field] = fieldShape
734
+ }
735
+ }
736
+ return new TemplateValueShape({ source, value: newProps })
737
+ }
738
+
589
739
  /**
590
740
  * Build an ObjectShape representing instance inputs from a base64-zipped JSON payload.
591
741
  * This is used by both subflow and action instances.
@@ -689,9 +839,29 @@ function buildInputsShapeFromZipped({
689
839
  )
690
840
  }
691
841
  }
692
- // Use the specialized shape or fallback to normalized value
693
- props[name] =
694
- inputShape ?? normalizeInputValue(value as string, uiType ?? parameter.type, record, logger)
842
+ // Use the specialized shape or fallback to normalized value.
843
+ // Complex-object (FlowObject/FlowArray) inputs: decode the $cv payload to a
844
+ // plain object so typed action references (Path 1) satisfy TypeScript types
845
+ // and the build's resolveComplexInput path re-encodes them correctly.
846
+ // If decode fails (empty payload / spoke references with no data), fall back
847
+ // to the verbatim string so Path 2 (snapshot/string reference) still works.
848
+ // applyTemplateValueSubFieldScripts reconstructs FDInlineScriptCallShape
849
+ // sub-fields for TemplateValue inputs that carry a `script` map.
850
+ let baseShape: unknown
851
+ if (inputShape) {
852
+ baseShape = inputShape
853
+ } else if (getComplexObjectPayloadType(value) !== undefined) {
854
+ // Only decode when the action schema is available (Path 1: typed action reference,
855
+ // definitionInputs is populated from sys_hub_action_input records).
856
+ // For Path 2 (snapshot/string reference, definitionInputs is undefined),
857
+ // keep the verbatim string so build can re-emit it byte-for-byte.
858
+ const decoded =
859
+ definitionInputs !== undefined ? decodeComplexObjectPayload(value as string) : undefined
860
+ baseShape = decoded !== undefined ? decoded : value
861
+ } else {
862
+ baseShape = normalizeInputValue(value as string, uiType ?? parameter.type, record, logger)
863
+ }
864
+ props[name] = applyTemplateValueSubFieldScripts(baseShape, script, record)
695
865
  }
696
866
  } catch (e) {
697
867
  const recordInfo = `${record.getTable()}.${record.getId().getValue()}`
@@ -941,6 +1111,22 @@ function processInputValue(
941
1111
  return primitiveValue
942
1112
  }
943
1113
 
1114
+ // Preserve path: the input value is already a serialized FD complex-object payload
1115
+ // (FlowObject/FlowArray). Re-emit it verbatim with the correct parameter.type instead
1116
+ // of reconstructing it — works for typed, string/sys_id, and spoke references alike.
1117
+ const preservedComplexType = getComplexObjectPayloadType(primitiveValue)
1118
+ if (preservedComplexType !== undefined) {
1119
+ return {
1120
+ name: ELEMENT_MAPPING_FIELD_ALIASES_REVERSE[inputName] ?? inputName,
1121
+ value: primitiveValue,
1122
+ displayValue: displayValues?.has(inputName) ? displayValues.get(inputName) : primitiveValue,
1123
+ scriptActive: false,
1124
+ parameter: {
1125
+ type: preservedComplexType,
1126
+ },
1127
+ }
1128
+ }
1129
+
944
1130
  let finalValue: unknown = primitiveValue
945
1131
  let finalType: unknown = options?.type ?? 'string'
946
1132
 
@@ -1091,7 +1277,13 @@ function validateMandatoryFields(
1091
1277
  definitionName?: string
1092
1278
  ): Record[] | undefined {
1093
1279
  const inputDefinitions = definition
1094
- ? definition.flat().filter((record) => record.getTable() === inputTable)
1280
+ ? definition
1281
+ .flat()
1282
+ .filter(
1283
+ (record) =>
1284
+ record.getTable() === inputTable &&
1285
+ getRefSysId(record.get('model')) === definition.getId().getValue()
1286
+ )
1095
1287
  : undefined
1096
1288
 
1097
1289
  if (inputDefinitions) {
@@ -1367,6 +1559,9 @@ async function prepareActionInstanceValueJson(
1367
1559
  properties: Object.fromEntries(mergedResults),
1368
1560
  })
1369
1561
 
1562
+ // Collect scripted sub-fields for any TemplateValue inputs
1563
+ const tvScripts = extractTemplateValueScripts(instanceInputs)
1564
+
1370
1565
  const result: unknown[] = []
1371
1566
 
1372
1567
  // When isActionDefString is true, there's no action definition in fluent
@@ -1428,6 +1623,18 @@ async function prepareActionInstanceValueJson(
1428
1623
  }
1429
1624
  }
1430
1625
 
1626
+ // Attach scripted sub-fields for any TemplateValue inputs
1627
+ if (tvScripts.size > 0) {
1628
+ for (const item of result) {
1629
+ if (typeof item === 'object' && item !== null && 'name' in item) {
1630
+ const scripts = tvScripts.get((item as globalThis.Record<string, unknown>)['name'] as string)
1631
+ if (scripts) {
1632
+ ;(item as globalThis.Record<string, unknown>)['script'] = scripts
1633
+ }
1634
+ }
1635
+ }
1636
+ }
1637
+
1431
1638
  return result
1432
1639
  }
1433
1640
 
@@ -1460,7 +1667,10 @@ async function prepareSubflowInstanceValueJson(
1460
1667
  properties: Object.fromEntries(mergedResults),
1461
1668
  })
1462
1669
 
1463
- return objShape
1670
+ // Collect scripted sub-fields for any TemplateValue inputs
1671
+ const tvScripts = extractTemplateValueScripts(instanceInputs)
1672
+
1673
+ const subflowResult = objShape
1464
1674
  .keys()
1465
1675
  .filter((key) => key !== 'waitForCompletion')
1466
1676
  .map((key) => {
@@ -1497,6 +1707,20 @@ async function prepareSubflowInstanceValueJson(
1497
1707
 
1498
1708
  return result
1499
1709
  })
1710
+
1711
+ // Attach scripted sub-fields for any TemplateValue inputs
1712
+ if (tvScripts.size > 0) {
1713
+ for (const item of subflowResult) {
1714
+ if (typeof item === 'object' && item !== null && 'name' in item) {
1715
+ const scripts = tvScripts.get((item as globalThis.Record<string, unknown>)['name'] as string)
1716
+ if (scripts) {
1717
+ ;(item as globalThis.Record<string, unknown>)['script'] = scripts
1718
+ }
1719
+ }
1720
+ }
1721
+ }
1722
+
1723
+ return subflowResult
1500
1724
  }
1501
1725
 
1502
1726
  /**
@@ -1560,6 +1784,8 @@ async function resolveObjectShapeRecursively(shape: ObjectShape, transform: Tran
1560
1784
  }
1561
1785
 
1562
1786
  resolvedObject[key] = resolvedArray
1787
+ } else if (valueShape instanceof FDInlineScriptCallShape) {
1788
+ resolvedObject[key] = 'fd-scripted'
1563
1789
  } else {
1564
1790
  // For all other shapes (primitives), get the value directly
1565
1791
  resolvedObject[key] = valueShape.getValue()
@@ -1601,6 +1827,36 @@ function wrapSpecialShape(shape: Shape, resolvedValue: unknown, source: Source):
1601
1827
  return resolvedValue
1602
1828
  }
1603
1829
 
1830
+ /**
1831
+ * Extracts scripted sub-fields from any TemplateValue inputs.
1832
+ * Returns a map of input name → { field: { scriptActive, script } } for inputs where
1833
+ * at least one sub-field is an FDInlineScriptCallShape (wfa.inlineScript()).
1834
+ * This map is then attached as a `script` property on the corresponding result item so
1835
+ * ServiceNow can store the script content separately (sys_hub_input_scripts).
1836
+ */
1837
+ function extractTemplateValueScripts(
1838
+ instanceInputs: ObjectShape
1839
+ ): Map<string, globalThis.Record<string, { scriptActive: true; script: string }>> {
1840
+ const result = new Map()
1841
+ for (const [key, shape] of instanceInputs.entries({ resolve: false })) {
1842
+ if (!shape.is(TemplateValueShape)) {
1843
+ continue
1844
+ }
1845
+ const scriptedFields: globalThis.Record<string, { scriptActive: true; script: string }> = {}
1846
+ for (const [field, fieldShape] of (shape as TemplateValueShape)
1847
+ .getTemplateValue()
1848
+ .entries({ resolve: false })) {
1849
+ if (fieldShape instanceof FDInlineScriptCallShape) {
1850
+ scriptedFields[field] = { scriptActive: true, script: fieldShape.getScriptContent() }
1851
+ }
1852
+ }
1853
+ if (Object.keys(scriptedFields).length > 0) {
1854
+ result.set(key, scriptedFields)
1855
+ }
1856
+ }
1857
+ return result
1858
+ }
1859
+
1604
1860
  /**
1605
1861
  * Checks for datapills and resolves them in instance inputs.
1606
1862
  * Similar to resolveObjectShapeRecursively but returns key-value pairs and handles special shapes.