@servicenow/sdk-build-plugins 4.5.0 → 4.6.0

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 (144) hide show
  1. package/dist/column-plugin.js +3 -7
  2. package/dist/column-plugin.js.map +1 -1
  3. package/dist/flow/flow-logic/flow-logic-diagnostics.js +5 -5
  4. package/dist/flow/flow-logic/flow-logic-diagnostics.js.map +1 -1
  5. package/dist/flow/plugins/flow-action-definition-plugin.js +1229 -54
  6. package/dist/flow/plugins/flow-action-definition-plugin.js.map +1 -1
  7. package/dist/flow/plugins/flow-data-pill-plugin.js +5 -2
  8. package/dist/flow/plugins/flow-data-pill-plugin.js.map +1 -1
  9. package/dist/flow/plugins/flow-definition-plugin.js +16 -42
  10. package/dist/flow/plugins/flow-definition-plugin.js.map +1 -1
  11. package/dist/flow/plugins/flow-diagnostics-plugin.d.ts +2 -2
  12. package/dist/flow/plugins/flow-diagnostics-plugin.js +2 -2
  13. package/dist/flow/plugins/flow-instance-plugin.js +68 -22
  14. package/dist/flow/plugins/flow-instance-plugin.js.map +1 -1
  15. package/dist/flow/plugins/step-definition-plugin.js +2 -1
  16. package/dist/flow/plugins/step-definition-plugin.js.map +1 -1
  17. package/dist/flow/plugins/step-instance-plugin.d.ts +9 -1
  18. package/dist/flow/plugins/step-instance-plugin.js +649 -136
  19. package/dist/flow/plugins/step-instance-plugin.js.map +1 -1
  20. package/dist/flow/plugins/wfa-datapill-plugin.js +20 -5
  21. package/dist/flow/plugins/wfa-datapill-plugin.js.map +1 -1
  22. package/dist/flow/post-install.js +1 -0
  23. package/dist/flow/post-install.js.map +1 -1
  24. package/dist/flow/utils/complex-object-resolver.js +4 -1
  25. package/dist/flow/utils/complex-object-resolver.js.map +1 -1
  26. package/dist/flow/utils/complex-objects.js +1 -1
  27. package/dist/flow/utils/complex-objects.js.map +1 -1
  28. package/dist/flow/utils/flow-constants.d.ts +66 -2
  29. package/dist/flow/utils/flow-constants.js +402 -6
  30. package/dist/flow/utils/flow-constants.js.map +1 -1
  31. package/dist/flow/utils/flow-io-to-record.d.ts +1 -1
  32. package/dist/flow/utils/flow-io-to-record.js +37 -16
  33. package/dist/flow/utils/flow-io-to-record.js.map +1 -1
  34. package/dist/flow/utils/flow-shapes.js +4 -0
  35. package/dist/flow/utils/flow-shapes.js.map +1 -1
  36. package/dist/flow/utils/label-cache-parser.d.ts +9 -2
  37. package/dist/flow/utils/label-cache-parser.js +32 -4
  38. package/dist/flow/utils/label-cache-parser.js.map +1 -1
  39. package/dist/flow/utils/pill-shape-helpers.d.ts +15 -0
  40. package/dist/flow/utils/pill-shape-helpers.js +35 -0
  41. package/dist/flow/utils/pill-shape-helpers.js.map +1 -0
  42. package/dist/flow/utils/pill-string-parser.js +1 -0
  43. package/dist/flow/utils/pill-string-parser.js.map +1 -1
  44. package/dist/flow/utils/schema-to-flow-object.d.ts +6 -1
  45. package/dist/flow/utils/schema-to-flow-object.js +131 -15
  46. package/dist/flow/utils/schema-to-flow-object.js.map +1 -1
  47. package/dist/flow/utils/utils.d.ts +1 -0
  48. package/dist/flow/utils/utils.js +6 -1
  49. package/dist/flow/utils/utils.js.map +1 -1
  50. package/dist/form-plugin.js +7 -9
  51. package/dist/form-plugin.js.map +1 -1
  52. package/dist/inbound-email-action-plugin.d.ts +10 -0
  53. package/dist/inbound-email-action-plugin.js +128 -0
  54. package/dist/inbound-email-action-plugin.js.map +1 -0
  55. package/dist/index.d.ts +4 -0
  56. package/dist/index.js +4 -0
  57. package/dist/index.js.map +1 -1
  58. package/dist/instance-scan-plugin.js +0 -5
  59. package/dist/instance-scan-plugin.js.map +1 -1
  60. package/dist/property-plugin.js +1 -1
  61. package/dist/property-plugin.js.map +1 -1
  62. package/dist/record-plugin.d.ts +7 -0
  63. package/dist/record-plugin.js +10 -2
  64. package/dist/record-plugin.js.map +1 -1
  65. package/dist/rest-api-plugin.js +8 -1
  66. package/dist/rest-api-plugin.js.map +1 -1
  67. package/dist/schedule-script/scheduled-script-plugin.js +8 -3
  68. package/dist/schedule-script/scheduled-script-plugin.js.map +1 -1
  69. package/dist/service-catalog/service-catalog-base.d.ts +18 -18
  70. package/dist/service-catalog/service-catalog-base.js +22 -22
  71. package/dist/service-catalog/service-catalog-base.js.map +1 -1
  72. package/dist/service-portal/header-footer-plugin.d.ts +2 -0
  73. package/dist/service-portal/header-footer-plugin.js +50 -0
  74. package/dist/service-portal/header-footer-plugin.js.map +1 -0
  75. package/dist/service-portal/menu-plugin.js +3 -22
  76. package/dist/service-portal/menu-plugin.js.map +1 -1
  77. package/dist/service-portal/page-plugin.js +3 -24
  78. package/dist/service-portal/page-plugin.js.map +1 -1
  79. package/dist/service-portal/page-route-map-plugin.d.ts +2 -0
  80. package/dist/service-portal/page-route-map-plugin.js +114 -0
  81. package/dist/service-portal/page-route-map-plugin.js.map +1 -0
  82. package/dist/service-portal/portal-plugin.js +21 -8
  83. package/dist/service-portal/portal-plugin.js.map +1 -1
  84. package/dist/service-portal/utils.d.ts +40 -2
  85. package/dist/service-portal/utils.js +283 -2
  86. package/dist/service-portal/utils.js.map +1 -1
  87. package/dist/service-portal/widget-plugin.js +9 -218
  88. package/dist/service-portal/widget-plugin.js.map +1 -1
  89. package/dist/static-content-plugin.js +4 -0
  90. package/dist/static-content-plugin.js.map +1 -1
  91. package/dist/table-plugin.js +190 -26
  92. package/dist/table-plugin.js.map +1 -1
  93. package/dist/ui-action-plugin.js +1 -4
  94. package/dist/ui-action-plugin.js.map +1 -1
  95. package/dist/ui-page-plugin.js +68 -13
  96. package/dist/ui-page-plugin.js.map +1 -1
  97. package/dist/view-plugin.js +8 -3
  98. package/dist/view-plugin.js.map +1 -1
  99. package/dist/workspace-plugin.js +39 -36
  100. package/dist/workspace-plugin.js.map +1 -1
  101. package/package.json +5 -4
  102. package/src/column-plugin.ts +3 -8
  103. package/src/flow/flow-logic/flow-logic-diagnostics.ts +5 -6
  104. package/src/flow/plugins/flow-action-definition-plugin.ts +1581 -61
  105. package/src/flow/plugins/flow-data-pill-plugin.ts +5 -2
  106. package/src/flow/plugins/flow-definition-plugin.ts +12 -47
  107. package/src/flow/plugins/flow-diagnostics-plugin.ts +2 -2
  108. package/src/flow/plugins/flow-instance-plugin.ts +98 -22
  109. package/src/flow/plugins/step-definition-plugin.ts +2 -1
  110. package/src/flow/plugins/step-instance-plugin.ts +772 -156
  111. package/src/flow/plugins/wfa-datapill-plugin.ts +25 -5
  112. package/src/flow/post-install.ts +1 -0
  113. package/src/flow/utils/complex-object-resolver.ts +4 -1
  114. package/src/flow/utils/complex-objects.ts +1 -1
  115. package/src/flow/utils/flow-constants.ts +421 -5
  116. package/src/flow/utils/flow-io-to-record.ts +43 -17
  117. package/src/flow/utils/flow-shapes.ts +4 -0
  118. package/src/flow/utils/label-cache-parser.ts +33 -4
  119. package/src/flow/utils/pill-shape-helpers.ts +42 -0
  120. package/src/flow/utils/pill-string-parser.ts +1 -0
  121. package/src/flow/utils/schema-to-flow-object.ts +183 -15
  122. package/src/flow/utils/utils.ts +12 -1
  123. package/src/form-plugin.ts +1 -3
  124. package/src/inbound-email-action-plugin.ts +145 -0
  125. package/src/index.ts +4 -0
  126. package/src/instance-scan-plugin.ts +0 -5
  127. package/src/property-plugin.ts +4 -1
  128. package/src/record-plugin.ts +14 -4
  129. package/src/rest-api-plugin.ts +7 -1
  130. package/src/schedule-script/scheduled-script-plugin.ts +14 -3
  131. package/src/service-catalog/service-catalog-base.ts +22 -22
  132. package/src/service-portal/header-footer-plugin.ts +57 -0
  133. package/src/service-portal/menu-plugin.ts +1 -23
  134. package/src/service-portal/page-plugin.ts +3 -28
  135. package/src/service-portal/page-route-map-plugin.ts +124 -0
  136. package/src/service-portal/portal-plugin.ts +33 -10
  137. package/src/service-portal/utils.ts +404 -3
  138. package/src/service-portal/widget-plugin.ts +14 -290
  139. package/src/static-content-plugin.ts +3 -0
  140. package/src/table-plugin.ts +226 -36
  141. package/src/ui-action-plugin.ts +1 -8
  142. package/src/ui-page-plugin.ts +76 -13
  143. package/src/view-plugin.ts +10 -4
  144. package/src/workspace-plugin.ts +43 -43
@@ -1,10 +1,1050 @@
1
- import { CallExpressionShape, deleteMultipleDiff, Plugin, Record, UndefinedShape } from '@servicenow/sdk-build-core'
2
- import { buildVariableRecords, complexObjectMatchesIoRecord } from '../utils/flow-io-to-record'
1
+ import {
2
+ ArrayShape,
3
+ CallExpressionShape,
4
+ deleteMultipleDiff,
5
+ IdentifierShape,
6
+ ObjectShape,
7
+ Plugin,
8
+ PropertyAccessShape,
9
+ type Record,
10
+ TemplateExpressionShape,
11
+ TemplateSpanShape,
12
+ TemplateValueShape,
13
+ UndefinedShape,
14
+ VariableStatementShape,
15
+ type Diagnostics,
16
+ type Source,
17
+ type Shape,
18
+ } from '@servicenow/sdk-build-core'
19
+ import { buildVariableRecords, buildVariableShapes, complexObjectMatchesIoRecord } from '../utils/flow-io-to-record'
3
20
  import { generateXML } from '../utils/flow-to-xml'
4
- import { slugifyString } from '../utils/flow-constants'
21
+ import {
22
+ slugifyString,
23
+ BUILT_IN_STEP_DEFINITIONS,
24
+ BUILT_IN_STEP_SYS_ID_NAME_MAP,
25
+ CORE_ACTIONS_SYS_ID_NAME_MAP,
26
+ ELEMENT_MAPPING_FIELD_ALIASES,
27
+ getVarEntryName,
28
+ getVarEntryType,
29
+ } from '../utils/flow-constants'
30
+ import { COLUMN_TYPE_TO_API } from '../../column/column-helper'
31
+ import { getAttributeValue } from '../utils/schema-to-flow-object'
32
+ import { normalizeInputValue } from './flow-instance-plugin'
5
33
  import { ArrowFunctionShape } from '../../arrow-function-plugin'
6
34
  import { createSdkDocEntry } from '../../utils'
7
- import { StepInstanceShape } from './step-instance-plugin'
35
+ import { FDInlineScriptCallShape } from './inline-script-plugin'
36
+ import { StepInstanceShape, StepInstancePlugin } from './step-instance-plugin'
37
+ import { NowIdShape } from '../../now-id-plugin'
38
+ import { NowIncludeShape } from '../../now-include-plugin'
39
+ import { getBuiltInStepIdentifier, getIdentifierFromRecord } from '../utils/utils'
40
+ import { convertPillStringToShape, detectPillPattern } from '../utils/pill-string-parser'
41
+ import { createLableCacheNameToTypeMap } from '../utils/label-cache-parser'
42
+ import { COLUMN_API_TO_TYPE } from '../../column/column-helper'
43
+ import { wrapWithDataPillCall, extractDataPillNames } from '../utils/pill-shape-helpers'
44
+
45
+ /** Shorthand for plain string-keyed object to avoid name clash with the SDK's Record type. */
46
+ type Props = { [key: string]: unknown }
47
+
48
+ /**
49
+ * Resolves inline scripts for a step's inputs by querying sys_hub_input_scripts descendants.
50
+ * Replaces 'fd-scripted' placeholder values with FDInlineScriptCallShape (wfa.inlineScript()).
51
+ */
52
+ function resolveInlineScripts(
53
+ inputsObj: Props,
54
+ stepSysId: string,
55
+ allInputScripts: Record[],
56
+ stepInstance: Record
57
+ ): void {
58
+ const inputScriptRecords = allInputScripts.filter(
59
+ (r: Record) => r.get('instance')?.asString()?.getValue() === stepSysId
60
+ )
61
+
62
+ for (const scriptRecord of inputScriptRecords) {
63
+ const inputName = scriptRecord.get('input_name')?.asString()?.getValue()
64
+ const scriptJson = scriptRecord.get('script')?.asString()?.getValue()
65
+ if (!inputName || !scriptJson) {
66
+ continue
67
+ }
68
+
69
+ let scriptData: { [key: string]: { scriptActive?: boolean; script?: string } }
70
+ try {
71
+ scriptData = JSON.parse(scriptJson)
72
+ } catch {
73
+ continue
74
+ }
75
+
76
+ const currentValue = inputsObj[inputName]
77
+
78
+ if (currentValue instanceof TemplateValueShape) {
79
+ // Replace fd-scripted sub-fields within TemplateValue with inline script shapes
80
+ const templateObj = currentValue.getTemplateValue()
81
+ const newProperties: Props = {}
82
+ const entries = templateObj.getValue() as globalThis.Record<string, unknown>
83
+ for (const [fieldKey, fieldValue] of Object.entries(entries)) {
84
+ const fieldScript = scriptData[fieldKey]
85
+ if (fieldScript?.scriptActive && fieldScript.script) {
86
+ newProperties[fieldKey] = new FDInlineScriptCallShape({
87
+ source: stepInstance,
88
+ scriptContent: fieldScript.script,
89
+ })
90
+ } else {
91
+ newProperties[fieldKey] = fieldValue
92
+ }
93
+ }
94
+ // Also add scripted fields not in original template
95
+ for (const [fieldKey, fieldScript] of Object.entries(scriptData)) {
96
+ if (fieldScript?.scriptActive && fieldScript.script && !(fieldKey in entries)) {
97
+ newProperties[fieldKey] = new FDInlineScriptCallShape({
98
+ source: stepInstance,
99
+ scriptContent: fieldScript.script,
100
+ })
101
+ }
102
+ }
103
+ inputsObj[inputName] = new TemplateValueShape({ source: stepInstance, value: newProperties })
104
+ }
105
+ }
106
+ }
107
+
108
+ /** Parameter name for the action body arrow function — aligned with subflow pattern */
109
+ const ACTION_PILL_PARAM_NAME = 'params'
110
+
111
+ /** Arrow delimiter used in label_cache labels — matches Flow Designer UI */
112
+ const LABEL_DELIMITER = '➛'
113
+
114
+ /**
115
+ * Generates a unique variable name from a label, appending _2, _3, etc. for duplicates.
116
+ */
117
+ function generateUniqueVarName(label: string, usedNames: Set<string>): string {
118
+ const base = slugifyString(label)
119
+ if (!usedNames.has(base)) {
120
+ return base
121
+ }
122
+ let counter = 2
123
+ while (usedNames.has(`${base}_${counter}`)) {
124
+ counter++
125
+ }
126
+ return `${base}_${counter}`
127
+ }
128
+
129
+ /** Step pill info collected during toRecord body processing. */
130
+ interface StepPillInfo {
131
+ cid: string
132
+ stepLabel: string
133
+ pillPath: string // e.g., "record.short_description"
134
+ dataType: string
135
+ }
136
+
137
+ /** Action dot-walk pill info (e.g., action.incident.description). */
138
+ interface ActionDotWalkPillInfo {
139
+ fullPath: string // e.g., "incident.description"
140
+ parentField: string // e.g., "incident"
141
+ columnName: string // e.g., "description"
142
+ dataType: string
143
+ }
144
+
145
+ /**
146
+ * Resolves a wfa.dataPill() CallExpressionShape to a pill string.
147
+ * Handles both action pills (params.inputs.xxx → {{action.xxx|type}}) and
148
+ * step pills (stepVar.prop → {{step[CID].prop|type}}).
149
+ * Returns undefined for non-pill shapes or unresolvable step references.
150
+ */
151
+ function resolveAnyPillFromShape(
152
+ fieldShape: Shape,
153
+ cidMap: Map<string, string>
154
+ ): { pill: string; isStep: boolean } | undefined {
155
+ if (!(fieldShape instanceof CallExpressionShape) || fieldShape.getCallee() !== 'wfa.dataPill') {
156
+ return undefined
157
+ }
158
+ const expressionArg = fieldShape.getArgument(0, false)
159
+ if (!(expressionArg instanceof PropertyAccessShape)) {
160
+ return undefined
161
+ }
162
+ const propertyNames: string[] = []
163
+ expressionArg.getElements().forEach((el: Shape) => {
164
+ if (el instanceof IdentifierShape) {
165
+ propertyNames.push(el.getName())
166
+ }
167
+ })
168
+ if (propertyNames.length < 2) {
169
+ return undefined
170
+ }
171
+ const typeArg = fieldShape.getArgument(1)
172
+ const dataType = typeArg?.ifString()?.getValue()
173
+ const typeSuffix = dataType ? `|${dataType}` : ''
174
+
175
+ if (propertyNames[1] === 'inputs') {
176
+ // Action pill: params.inputs.xxx → {{action.xxx|type}}
177
+ const pathParts = propertyNames.slice(2)
178
+ return { pill: `{{action.${pathParts.join('.')}${typeSuffix}}}`, isStep: false }
179
+ }
180
+ // Step pill: stepVar.prop → {{step[CID].prop|type}}
181
+ const varName = propertyNames[0]
182
+ const stepCid = varName ? cidMap.get(varName) : undefined
183
+ if (!stepCid) {
184
+ return undefined
185
+ }
186
+ const pathParts = propertyNames.slice(1)
187
+ return { pill: `{{step[${stepCid}].${pathParts.join('.')}${typeSuffix}}}`, isStep: true }
188
+ }
189
+
190
+ /**
191
+ * Strips the |type suffix from a pill string.
192
+ * e.g., "{{step[CID].record.number|string}}" → "{{step[CID].record.number}}"
193
+ * Note: Mirrors stripPillType in step-instance-plugin.ts (kept separate to avoid exporting private functions).
194
+ */
195
+ function stripPillTypeSuffix(pill: string): string {
196
+ return pill.replace(/\|[^}]+/, '')
197
+ }
198
+
199
+ /** Regex for extracting step pill type annotations — same pattern as STEP_PILL_TYPE_REGEX in step-instance-plugin.ts */
200
+ const STEP_PILL_WITH_TYPE_REGEX = /\{\{step\[([^\]]+)\]\.([^|}]+)(?:\|([^}]+))?\}\}/g
201
+
202
+ /** Extracts the step pill type annotation from a pill string and stores it in pillTypeMap for label_cache. */
203
+ function collectStepPillType(pill: string, pillTypeMap: Map<string, string>): void {
204
+ for (const match of pill.matchAll(STEP_PILL_WITH_TYPE_REGEX)) {
205
+ const [, cid, pillPath, dataType] = match
206
+ if (cid && pillPath && dataType) {
207
+ pillTypeMap.set(`${cid}::${pillPath}`, dataType)
208
+ }
209
+ }
210
+ }
211
+
212
+ /** Creates a sys_element_mapping record — shared by all cases in resolveUnresolvedStepPills. */
213
+ function createElementMapping(
214
+ factory: {
215
+ createRecord: (opts: {
216
+ source: Shape
217
+ table: string
218
+ properties: globalThis.Record<string, unknown>
219
+ }) => Promise<Record>
220
+ },
221
+ source: Shape,
222
+ field: string,
223
+ id: string,
224
+ stepDefinitionSysId: string,
225
+ value: string
226
+ ): Promise<Record> {
227
+ return factory.createRecord({
228
+ source,
229
+ table: 'sys_element_mapping',
230
+ properties: {
231
+ field,
232
+ id,
233
+ table: `var__m_sys_flow_step_definition_input_${stepDefinitionSysId}`,
234
+ value,
235
+ },
236
+ })
237
+ }
238
+
239
+ /**
240
+ * Resolves a TemplateExpressionShape, replacing step pill spans with resolved pill strings.
241
+ * Action pills and plain text spans are preserved from the original template.
242
+ * Returns the full resolved string and whether any step pills were resolved.
243
+ */
244
+ function resolveStepPillsInTemplate(
245
+ templateShape: TemplateExpressionShape,
246
+ cidMap: Map<string, string>,
247
+ pillTypeMap?: Map<string, string>
248
+ ): { result: string; hasStepPills: boolean } | undefined {
249
+ let result = templateShape.getLiteralText()
250
+ let hasStepPills = false
251
+ for (const span of templateShape.getSpans()) {
252
+ const expr = span.getExpression()
253
+ const resolved = resolveAnyPillFromShape(expr as Shape, cidMap)
254
+ if (resolved) {
255
+ if (resolved.isStep) {
256
+ if (pillTypeMap) {
257
+ collectStepPillType(resolved.pill, pillTypeMap)
258
+ }
259
+ hasStepPills = true
260
+ }
261
+ result += stripPillTypeSuffix(resolved.pill)
262
+ } else {
263
+ // Expression may be an already-resolved PillShape (from SDK auto-processing).
264
+ // Strip type suffix so action pills match the expected format.
265
+ const val = String(expr.getValue?.() ?? '')
266
+ result += val.startsWith('{{') ? stripPillTypeSuffix(val) : val
267
+ }
268
+ result += span.getLiteralText()
269
+ }
270
+ if (!hasStepPills) {
271
+ return undefined
272
+ }
273
+ return { result, hasStepPills }
274
+ }
275
+
276
+ /**
277
+ * After the step loop, cidMap is fully populated but auto-processed Records may contain
278
+ * unresolved step pills (empty sys_variable_value or broken sys_element_mapping values).
279
+ * This function walks each StepInstanceShape's inputs, re-resolves step pills with the
280
+ * populated cidMap, and creates correct sys_element_mapping records via factory.
281
+ * Coalesce on [field, id] ensures these override any broken records from auto-processing.
282
+ */
283
+ interface StepInfo {
284
+ shape: StepInstanceShape
285
+ stepInstanceSysId: string
286
+ stepDefinitionSysId: string
287
+ }
288
+
289
+ async function resolveUnresolvedStepPills(
290
+ steps: StepInfo[],
291
+ cidMap: Map<string, string>,
292
+ pillTypeMap: Map<string, string>,
293
+ factory: {
294
+ createRecord: (opts: {
295
+ source: Shape
296
+ table: string
297
+ properties: globalThis.Record<string, unknown>
298
+ }) => Promise<Record>
299
+ }
300
+ ): Promise<Record[]> {
301
+ if (cidMap.size === 0 || steps.length === 0) {
302
+ return []
303
+ }
304
+ const newRecords: Record[] = []
305
+
306
+ for (const { shape: stepShape, stepInstanceSysId, stepDefinitionSysId } of steps) {
307
+ const inputs = stepShape.getInputs() as ObjectShape | undefined
308
+ if (!inputs) {
309
+ continue
310
+ }
311
+
312
+ const valuesProperties = inputs.properties({ resolve: false })
313
+
314
+ for (const [key, valueShape] of Object.entries(valuesProperties)) {
315
+ if (key === 'inputVariables' || key === 'outputVariables' || key === 'errorHandlingType') {
316
+ continue
317
+ }
318
+
319
+ // Case 1: Simple step pill — wfa.dataPill(stepVar.prop, 'type')
320
+ const resolved = resolveAnyPillFromShape(valueShape, cidMap)
321
+ if (resolved?.isStep) {
322
+ collectStepPillType(resolved.pill, pillTypeMap)
323
+ newRecords.push(
324
+ await createElementMapping(
325
+ factory,
326
+ stepShape,
327
+ key,
328
+ stepInstanceSysId,
329
+ stepDefinitionSysId,
330
+ stripPillTypeSuffix(resolved.pill)
331
+ )
332
+ )
333
+ continue
334
+ }
335
+
336
+ // Case 2: TemplateExpressionShape with step pills
337
+ if (valueShape.is(TemplateExpressionShape)) {
338
+ const templateResolved = resolveStepPillsInTemplate(
339
+ valueShape as TemplateExpressionShape,
340
+ cidMap,
341
+ pillTypeMap
342
+ )
343
+ if (templateResolved) {
344
+ newRecords.push(
345
+ await createElementMapping(
346
+ factory,
347
+ stepShape,
348
+ key,
349
+ stepInstanceSysId,
350
+ stepDefinitionSysId,
351
+ templateResolved.result
352
+ )
353
+ )
354
+ }
355
+ continue
356
+ }
357
+
358
+ // Case 3: TemplateValueShape — scan nested fields for step pills
359
+ if (valueShape.is(TemplateValueShape)) {
360
+ const templateObj = (valueShape as TemplateValueShape).getTemplateValue()
361
+ const entries: string[] = []
362
+ let hasStepPills = false
363
+
364
+ for (const [field, fieldShape] of templateObj.entries({ resolve: false })) {
365
+ const fieldResolved = resolveAnyPillFromShape(fieldShape, cidMap)
366
+ if (fieldResolved?.isStep) {
367
+ collectStepPillType(fieldResolved.pill, pillTypeMap)
368
+ entries.push(`${field}=${stripPillTypeSuffix(fieldResolved.pill)}`)
369
+ hasStepPills = true
370
+ } else if (fieldShape.is(TemplateExpressionShape)) {
371
+ const resolved = resolveStepPillsInTemplate(
372
+ fieldShape as TemplateExpressionShape,
373
+ cidMap,
374
+ pillTypeMap
375
+ )
376
+ if (resolved) {
377
+ entries.push(`${field}=${resolved.result}`)
378
+ hasStepPills = true
379
+ } else {
380
+ const val = fieldShape.getValue?.()
381
+ entries.push(`${field}=${String(val ?? '')}`)
382
+ }
383
+ } else {
384
+ const val = String(fieldShape.getValue?.() ?? '')
385
+ entries.push(`${field}=${val.startsWith('{{') ? stripPillTypeSuffix(val) : val}`)
386
+ }
387
+ }
388
+
389
+ if (hasStepPills) {
390
+ newRecords.push(
391
+ await createElementMapping(
392
+ factory,
393
+ stepShape,
394
+ key,
395
+ stepInstanceSysId,
396
+ stepDefinitionSysId,
397
+ entries.join('^')
398
+ )
399
+ )
400
+ }
401
+ }
402
+ }
403
+ }
404
+ return newRecords
405
+ }
406
+
407
+ /**
408
+ * Extracts step pill info from sys_element_mapping records created during step processing.
409
+ * Scans the `value` field of each sys_element_mapping record for step pill patterns
410
+ * and deduplicates by (cid, pillPath).
411
+ *
412
+ * This follows the same post-processing pattern as flow-definition-plugin which extracts
413
+ * pills from already-created records rather than during shape transformation.
414
+ */
415
+ function extractStepPillsFromRecords(
416
+ records: Record[],
417
+ cidToLabelMap: Map<string, string>,
418
+ pillTypeMap?: Map<string, string>
419
+ ): StepPillInfo[] {
420
+ const pills: StepPillInfo[] = []
421
+ const seen = new Set<string>()
422
+
423
+ for (const topRec of records) {
424
+ // sys_element_mapping records are nested inside step instance records;
425
+ // flat() expands them so we can scan pill values.
426
+ for (const rec of topRec.flat()) {
427
+ if (rec.getTable() !== 'sys_element_mapping') {
428
+ continue
429
+ }
430
+ const value = rec.get('value')?.asString()?.getValue() ?? ''
431
+ const matches = value.matchAll(STEP_PILL_WITH_TYPE_REGEX)
432
+ for (const match of matches) {
433
+ const [, cid, pillPath, dataType] = match
434
+ if (!cid || !pillPath) {
435
+ continue
436
+ }
437
+ const key = `${cid}::${pillPath}`
438
+ if (seen.has(key)) {
439
+ continue
440
+ }
441
+ seen.add(key)
442
+ // Prefer type from pillTypeMap (collected from wfa.dataPill() args before stripping),
443
+ // fall back to regex capture from element_mapping, then default to 'string'
444
+ const resolvedType = pillTypeMap?.get(key) ?? dataType ?? 'string'
445
+ pills.push({
446
+ cid,
447
+ stepLabel: cidToLabelMap.get(cid) ?? '',
448
+ pillPath,
449
+ dataType: resolvedType,
450
+ })
451
+ }
452
+ }
453
+ }
454
+ return pills
455
+ }
456
+
457
+ /**
458
+ * Extracts action dot-walk pill info from sys_element_mapping records.
459
+ * Scans for {{action.X.Y...}} patterns where the path has 2+ segments (i.e., deeper than top-level input).
460
+ * Deduplicates by full path. Uses pillTypeMap for type resolution.
461
+ */
462
+ function extractActionDotWalkPills(records: Record[], pillTypeMap?: Map<string, string>): ActionDotWalkPillInfo[] {
463
+ const pills: ActionDotWalkPillInfo[] = []
464
+ const seen = new Set<string>()
465
+ const regex = /\{\{action\.([^|}]+)(?:\|([^}]+))?\}\}/g
466
+
467
+ for (const topRec of records) {
468
+ for (const rec of topRec.flat()) {
469
+ if (rec.getTable() !== 'sys_element_mapping') {
470
+ continue
471
+ }
472
+ const value = rec.get('value')?.asString()?.getValue() ?? ''
473
+ for (const match of value.matchAll(regex)) {
474
+ const [, path, dataType] = match
475
+ if (!path) {
476
+ continue
477
+ }
478
+ const segments = path.split('.')
479
+ // Only dot-walk pills (2+ segments) — top-level ones are already in inputsConfig
480
+ if (segments.length < 2) {
481
+ continue
482
+ }
483
+ if (seen.has(path)) {
484
+ continue
485
+ }
486
+ seen.add(path)
487
+ const resolvedType = pillTypeMap?.get(`action::${path}`) ?? dataType ?? 'string'
488
+ pills.push({
489
+ fullPath: path,
490
+ parentField: segments[0] ?? '',
491
+ columnName: segments[segments.length - 1] ?? '',
492
+ dataType: resolvedType,
493
+ })
494
+ }
495
+ }
496
+ }
497
+ return pills
498
+ }
499
+
500
+ /** Converts snake_case to Title Case. e.g., "short_description" → "Short Description" */
501
+ function titleCase(s: string): string {
502
+ return s.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase())
503
+ }
504
+
505
+ /**
506
+ * Builds the label_cache JSON string from action input definitions, step output pills,
507
+ * action dot-walk pills, and step status entries — matching the format the Flow Designer UI generates.
508
+ *
509
+ * @param inputsConfig - The ObjectShape for the action's inputs configuration
510
+ * @param opts.stepPills - Step output pill info collected during body processing
511
+ * @param opts.dotWalkPills - Action dot-walk pill info (e.g., action.incident.description)
512
+ * @param opts.cidToLabelMap - Map of step CID → label, used for __step_status__ entries
513
+ * @returns JSON string of label cache entries, or empty string if no inputs
514
+ */
515
+ function buildActionLabelCache(
516
+ inputsConfig: ObjectShape,
517
+ opts?: {
518
+ stepPills?: StepPillInfo[]
519
+ dotWalkPills?: ActionDotWalkPillInfo[]
520
+ cidToLabelMap?: Map<string, string>
521
+ }
522
+ ): string {
523
+ const { stepPills, dotWalkPills, cidToLabelMap } = opts ?? {}
524
+ const entries: unknown[] = []
525
+
526
+ // Action input entries — also build inputLabelMap for dot-walk resolution in a single pass
527
+ const inputLabelMap = new Map<string, string>()
528
+ for (const [fieldName, fieldShape] of inputsConfig.entries({ resolve: false })) {
529
+ if (!(fieldShape instanceof CallExpressionShape)) {
530
+ continue
531
+ }
532
+ const columnApiName = fieldShape.getCallee()
533
+ const baseType = COLUMN_API_TO_TYPE[columnApiName] ?? 'string'
534
+ const config = fieldShape.getArgument(0)?.ifObject()?.asObject()
535
+ const label = config?.get('label')?.ifString()?.getValue() ?? fieldName
536
+ const referenceTable = config?.get('referenceTable')?.ifString()?.getValue() ?? ''
537
+ const referenceDisplay = referenceTable ? label : ''
538
+
539
+ inputLabelMap.set(fieldName, label)
540
+
541
+ entries.push({
542
+ name: `{{action.${fieldName}}}`,
543
+ label: `action${LABEL_DELIMITER}${label}`,
544
+ type: 'action',
545
+ ref: referenceTable,
546
+ reference_display: referenceDisplay,
547
+ base_type: baseType,
548
+ parent_table_name: '',
549
+ column_name: '',
550
+ choices: null,
551
+ attributes: {},
552
+ })
553
+ }
554
+
555
+ // Action dot-walk pill entries (e.g., action.incident.description)
556
+ if (dotWalkPills) {
557
+ for (const pill of dotWalkPills) {
558
+ const segments = pill.fullPath.split('.')
559
+ // Build label with all segments: action➛Incident➛Caller Id➛Email
560
+ const labelParts = segments.map((seg, i) => (i === 0 ? (inputLabelMap.get(seg) ?? seg) : titleCase(seg)))
561
+ const displayLabel = titleCase(pill.columnName)
562
+ entries.push({
563
+ name: `{{action.${pill.fullPath}}}`,
564
+ label: `action${LABEL_DELIMITER}${labelParts.join(LABEL_DELIMITER)}`,
565
+ type: 'action',
566
+ ref: '',
567
+ reference_display: displayLabel,
568
+ base_type: pill.dataType,
569
+ parent_table_name: pill.parentField,
570
+ column_name: pill.columnName,
571
+ choices: null,
572
+ attributes: {},
573
+ })
574
+ }
575
+ }
576
+
577
+ // __step_status__ entries for each step
578
+ if (cidToLabelMap) {
579
+ for (const [cid, stepLabel] of cidToLabelMap.entries()) {
580
+ entries.push({
581
+ name: `{{step[${cid}].__step_status__}}`,
582
+ label: `step${LABEL_DELIMITER}${stepLabel}${LABEL_DELIMITER}Step Status`,
583
+ type: 'step',
584
+ ref: '',
585
+ reference_display: '',
586
+ base_type: 'object',
587
+ parent_table_name: '',
588
+ column_name: '',
589
+ choices: null,
590
+ attributes: {},
591
+ })
592
+ }
593
+ }
594
+
595
+ // Step output pill entries
596
+ if (stepPills) {
597
+ for (const pill of stepPills) {
598
+ // Split path: "record.short_description" → column_name = "short_description"
599
+ const pathParts = pill.pillPath.split('.')
600
+ const columnName = pathParts[pathParts.length - 1] ?? ''
601
+ const displayLabel = titleCase(columnName)
602
+
603
+ entries.push({
604
+ name: `{{step[${pill.cid}].${pill.pillPath}}}`,
605
+ label: `step${LABEL_DELIMITER}${pill.stepLabel}${LABEL_DELIMITER}${displayLabel}`,
606
+ type: 'step',
607
+ ref: '',
608
+ reference_display: displayLabel,
609
+ base_type: pill.dataType,
610
+ parent_table_name: '',
611
+ column_name: columnName,
612
+ choices: null,
613
+ attributes: {},
614
+ })
615
+ }
616
+ }
617
+
618
+ return entries.length > 0 ? JSON.stringify(entries) : ''
619
+ }
620
+
621
+ /**
622
+ * Converts a string value containing action pills ({{action.xxx}}) into a wfa.dataPill() shape.
623
+ * Handles both simple pills (entire value is a pill) and template pills (mixed text + pills).
624
+ *
625
+ * @returns The converted shape, or undefined if no pills were found/converted.
626
+ */
627
+ function convertActionPillToShape(
628
+ value: string,
629
+ source: Source,
630
+ diagnostics: Diagnostics,
631
+ labelCacheMap?: Map<string, string>
632
+ ): CallExpressionShape | TemplateExpressionShape | undefined {
633
+ const pattern = detectPillPattern(value)
634
+ if (pattern === 'none') {
635
+ return undefined
636
+ }
637
+
638
+ const shape = convertPillStringToShape(value, source, diagnostics, ACTION_PILL_PARAM_NAME)
639
+ if (!shape) {
640
+ return undefined
641
+ }
642
+
643
+ const pillNames = extractDataPillNames(value)
644
+
645
+ // Single pill: wrap the PropertyAccessShape with wfa.dataPill()
646
+ if (shape instanceof PropertyAccessShape) {
647
+ const pillName = pillNames[0]
648
+ const dataType = (pillName && labelCacheMap?.get(pillName)) || 'string'
649
+ return wrapWithDataPillCall(shape, source, dataType)
650
+ }
651
+
652
+ if (shape instanceof IdentifierShape) {
653
+ return wrapWithDataPillCall(shape, source)
654
+ }
655
+
656
+ // Template expression: wrap each PropertyAccessShape span with wfa.dataPill()
657
+ if (shape instanceof TemplateExpressionShape) {
658
+ const originalSpans = shape.getSpans()
659
+ const wrappedSpans: TemplateSpanShape[] = []
660
+ let pillIndex = 0
661
+
662
+ for (const span of originalSpans) {
663
+ const expression = span.getExpression()
664
+ const literalText = span.getLiteralText()
665
+
666
+ if (expression instanceof PropertyAccessShape || expression instanceof IdentifierShape) {
667
+ const pillName = pillNames[pillIndex]
668
+ const dataType = (pillName && labelCacheMap?.get(pillName)) || 'string'
669
+ const wrappedExpr = wrapWithDataPillCall(expression, source, dataType)
670
+ pillIndex++
671
+
672
+ wrappedSpans.push(
673
+ new TemplateSpanShape({
674
+ source,
675
+ expression: wrappedExpr,
676
+ literalText,
677
+ })
678
+ )
679
+ } else {
680
+ wrappedSpans.push(span)
681
+ }
682
+ }
683
+
684
+ return new TemplateExpressionShape({
685
+ source,
686
+ literalText: shape.getLiteralText(),
687
+ spans: wrappedSpans,
688
+ })
689
+ }
690
+
691
+ return undefined
692
+ }
693
+
694
+ /**
695
+ * Duck-typing check to determine if a value is a Shape instance.
696
+ * Since Shape is imported as a type-only import, we can't use instanceof.
697
+ */
698
+ function isShapeInstance(val: unknown): boolean {
699
+ return (
700
+ val instanceof TemplateValueShape ||
701
+ val instanceof CallExpressionShape ||
702
+ val instanceof ObjectShape ||
703
+ val instanceof ArrayShape ||
704
+ val instanceof NowIncludeShape ||
705
+ val instanceof FDInlineScriptCallShape
706
+ )
707
+ }
708
+
709
+ /**
710
+ * Processes all values in an inputsObj, converting action pill strings to wfa.dataPill() shapes.
711
+ * Also handles pills inside TemplateValueShape fields and nested objects like inputVariables.
712
+ */
713
+ function convertActionPillsInInputs(
714
+ inputsObj: Props,
715
+ source: Source,
716
+ diagnostics: Diagnostics,
717
+ labelCacheMap?: Map<string, string>
718
+ ): void {
719
+ for (const [key, val] of Object.entries(inputsObj)) {
720
+ if (typeof val === 'string' && detectPillPattern(val) !== 'none') {
721
+ const converted = convertActionPillToShape(val, source, diagnostics, labelCacheMap)
722
+ if (converted) {
723
+ inputsObj[key] = converted
724
+ }
725
+ } else if (val instanceof TemplateValueShape) {
726
+ // Process pills inside TemplateValue fields.
727
+ // Use .properties() to get Shape instances directly, not .getValue()
728
+ // which calls base Shape.getValue() returning Symbols for non-primitive shapes.
729
+ const templateProps = val.getTemplateValue().properties()
730
+ let hasChanges = false
731
+ const newTemplateValue: globalThis.Record<string, unknown> = {}
732
+
733
+ for (const [field, fieldShape] of Object.entries(templateProps)) {
734
+ if (fieldShape.isString()) {
735
+ const fieldVal = fieldShape.getValue()
736
+ if (detectPillPattern(fieldVal) !== 'none') {
737
+ const converted = convertActionPillToShape(fieldVal, source, diagnostics, labelCacheMap)
738
+ if (converted) {
739
+ newTemplateValue[field] = converted
740
+ hasChanges = true
741
+ continue
742
+ }
743
+ }
744
+ }
745
+ newTemplateValue[field] = fieldShape
746
+ }
747
+
748
+ if (hasChanges) {
749
+ inputsObj[key] = new TemplateValueShape({ source, value: newTemplateValue })
750
+ }
751
+ } else if (val && typeof val === 'object' && !Array.isArray(val) && !isShapeInstance(val)) {
752
+ // Recursively process nested plain objects (e.g., inputVariables, outputVariables)
753
+ // This handles script step input variables which are nested objects with 'value' fields
754
+ const nestedObj = val as Props
755
+ for (const nestedVal of Object.values(nestedObj)) {
756
+ if (
757
+ nestedVal &&
758
+ typeof nestedVal === 'object' &&
759
+ !Array.isArray(nestedVal) &&
760
+ !isShapeInstance(nestedVal)
761
+ ) {
762
+ const innerObj = nestedVal as Props
763
+ // Check if this nested object has a 'value' field with a pill string
764
+ if (typeof innerObj['value'] === 'string' && detectPillPattern(innerObj['value']) !== 'none') {
765
+ const converted = convertActionPillToShape(
766
+ innerObj['value'],
767
+ source,
768
+ diagnostics,
769
+ labelCacheMap
770
+ )
771
+ if (converted) {
772
+ innerObj['value'] = converted
773
+ }
774
+ }
775
+ }
776
+ }
777
+ }
778
+ }
779
+ }
780
+
781
+ /** Regex to find step[UUID] pills */
782
+ const STEP_PILL_REGEX =
783
+ /\{\{step\[([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\]\.([^|}]+)(?:\|[^}]*)?\}\}/g
784
+
785
+ /**
786
+ * Converts step output datapill strings ({{step[UUID].property}}) in inputsObj to
787
+ * wfa.dataPill(varName.property, 'type') using the CID-to-identifier map.
788
+ * Handles both simple pills and template patterns (pill mixed with text).
789
+ */
790
+ function convertStepPillsInInputs(
791
+ inputsObj: Props,
792
+ source: Source,
793
+ cidToIdentifierMap: Map<string, IdentifierShape>,
794
+ labelCacheMap?: Map<string, string>
795
+ ): void {
796
+ for (const [key, val] of Object.entries(inputsObj)) {
797
+ if (typeof val === 'string') {
798
+ const converted = convertStepPillString(val, source, cidToIdentifierMap, labelCacheMap)
799
+ if (converted) {
800
+ inputsObj[key] = converted
801
+ }
802
+ } else if (val instanceof TemplateExpressionShape) {
803
+ // A TemplateExpressionShape may still contain raw {{step[UUID].xxx}} pills in its
804
+ // literal text segments if convertActionPillsInInputs converted action pills first,
805
+ // turning the original string into a template expression with step pills left in text.
806
+ const resolved = resolveStepPillsInTemplateExpression(val, source, cidToIdentifierMap, labelCacheMap)
807
+ if (resolved) {
808
+ inputsObj[key] = resolved
809
+ }
810
+ } else if (val instanceof TemplateValueShape) {
811
+ // Recurse into TemplateValue properties to resolve step pills.
812
+ // Use .properties() to get Shape instances directly, not .getValue()
813
+ // which calls base Shape.getValue() returning Symbols for non-primitive shapes.
814
+ const templateProps = val.getTemplateValue().properties()
815
+ let hasChanges = false
816
+ const newTemplateValue: globalThis.Record<string, unknown> = {}
817
+
818
+ for (const [field, fieldShape] of Object.entries(templateProps)) {
819
+ if (fieldShape.isString()) {
820
+ const strVal = fieldShape.getValue()
821
+ const converted = convertStepPillString(strVal, source, cidToIdentifierMap, labelCacheMap)
822
+ if (converted) {
823
+ newTemplateValue[field] = converted
824
+ hasChanges = true
825
+ continue
826
+ }
827
+ }
828
+ // Keep the original Shape instance as-is (already processed by convertActionPillsInInputs)
829
+ newTemplateValue[field] = fieldShape
830
+ }
831
+
832
+ if (hasChanges) {
833
+ inputsObj[key] = new TemplateValueShape({ source, value: newTemplateValue })
834
+ }
835
+ }
836
+ }
837
+ }
838
+
839
+ /**
840
+ * Resolves a single step pill match to a wfa.dataPill() CallExpressionShape.
841
+ */
842
+ function resolveStepPillMatch(
843
+ uuid: string,
844
+ property: string,
845
+ source: Source,
846
+ cidToIdentifierMap: Map<string, IdentifierShape>,
847
+ labelCacheMap?: Map<string, string>
848
+ ): CallExpressionShape | undefined {
849
+ const identifier = cidToIdentifierMap.get(uuid)
850
+ if (!identifier) {
851
+ return undefined
852
+ }
853
+
854
+ const expression = new PropertyAccessShape({
855
+ source,
856
+ elements: [identifier, property],
857
+ })
858
+
859
+ const pillName = `step[${uuid}].${property}`
860
+ const dataType = labelCacheMap?.get(pillName) || 'string'
861
+
862
+ return wrapWithDataPillCall(expression, source, dataType)
863
+ }
864
+
865
+ /**
866
+ * Resolves step pills remaining in the literal text segments of a TemplateExpressionShape.
867
+ * This handles the case where convertActionPillsInInputs converted action pills first,
868
+ * producing a TemplateExpressionShape whose head/span literal texts still contain
869
+ * raw {{step[UUID].xxx}} pill references that need to be resolved.
870
+ *
871
+ * Algorithm: flatten the template into alternating text/expression segments, expand any
872
+ * step pills found in text segments, then rebuild a TemplateExpressionShape.
873
+ */
874
+ function resolveStepPillsInTemplateExpression(
875
+ expr: TemplateExpressionShape,
876
+ source: Source,
877
+ cidToIdentifierMap: Map<string, IdentifierShape>,
878
+ labelCacheMap?: Map<string, string>
879
+ ): TemplateExpressionShape | undefined {
880
+ type Segment = { kind: 'text'; value: string } | { kind: 'expr'; value: Shape }
881
+
882
+ // Flatten into alternating text/expression segments
883
+ const segments: Segment[] = [{ kind: 'text', value: expr.getLiteralText() }]
884
+ for (const span of expr.getSpans()) {
885
+ segments.push({ kind: 'expr', value: span.getExpression() })
886
+ segments.push({ kind: 'text', value: span.getLiteralText() })
887
+ }
888
+
889
+ // Expand step pills in each text segment
890
+ const expanded: Segment[] = []
891
+ let resolvedCount = 0
892
+
893
+ for (const seg of segments) {
894
+ if (seg.kind === 'expr') {
895
+ expanded.push(seg)
896
+ continue
897
+ }
898
+
899
+ const text = seg.value
900
+ const matches = Array.from(text.matchAll(STEP_PILL_REGEX))
901
+ if (matches.length === 0) {
902
+ expanded.push(seg)
903
+ continue
904
+ }
905
+
906
+ let lastIdx = 0
907
+ for (const match of matches) {
908
+ const uuid = match[1]
909
+ const property = match[2]
910
+ if (!uuid || !property) {
911
+ continue
912
+ }
913
+
914
+ const pillExpr = resolveStepPillMatch(uuid, property, source, cidToIdentifierMap, labelCacheMap)
915
+ if (!pillExpr) {
916
+ // Unresolved pill (CID not in identifier map) — leave the raw {{step[UUID].xxx}}
917
+ // text in the output. This is intentional graceful degradation: the pill text is
918
+ // preserved as-is rather than silently dropped, so the output remains debuggable.
919
+ continue
920
+ }
921
+
922
+ resolvedCount++
923
+ const matchStart = match.index ?? 0
924
+ expanded.push({ kind: 'text', value: text.substring(lastIdx, matchStart) })
925
+ expanded.push({ kind: 'expr', value: pillExpr })
926
+ lastIdx = matchStart + match[0].length
927
+ }
928
+ expanded.push({ kind: 'text', value: text.substring(lastIdx) })
929
+ }
930
+
931
+ if (resolvedCount === 0) {
932
+ return undefined
933
+ }
934
+
935
+ // Merge consecutive text segments
936
+ const merged: Segment[] = []
937
+ for (const seg of expanded) {
938
+ const last = merged[merged.length - 1]
939
+ if (seg.kind === 'text' && last?.kind === 'text') {
940
+ last.value += seg.value
941
+ } else {
942
+ merged.push(seg)
943
+ }
944
+ }
945
+
946
+ // Rebuild TemplateExpressionShape: first text is the head, then (expr, text) pairs form spans
947
+ const head = merged[0]?.kind === 'text' ? merged[0].value : ''
948
+ const startIdx = merged[0]?.kind === 'text' ? 1 : 0
949
+ const newSpans: TemplateSpanShape[] = []
950
+
951
+ for (let i = startIdx; i < merged.length; i += 2) {
952
+ const exprSeg = merged[i]
953
+ const textSeg = merged[i + 1]
954
+ if (exprSeg?.kind === 'expr') {
955
+ newSpans.push(
956
+ new TemplateSpanShape({
957
+ source,
958
+ expression: exprSeg.value,
959
+ literalText: textSeg?.kind === 'text' ? textSeg.value : '',
960
+ })
961
+ )
962
+ }
963
+ }
964
+
965
+ return new TemplateExpressionShape({ source, literalText: head, spans: newSpans })
966
+ }
967
+
968
+ /**
969
+ * Converts step pill strings to wfa.dataPill() shapes.
970
+ * Handles both:
971
+ * - Simple: "{{step[UUID].record.short_description}}" → wfa.dataPill(varName.record.short_description, 'string')
972
+ * - Template: "{{step[UUID].record.short_description}} is text" → `${wfa.dataPill(...)} is text`
973
+ */
974
+ function convertStepPillString(
975
+ value: string,
976
+ source: Source,
977
+ cidToIdentifierMap: Map<string, IdentifierShape>,
978
+ labelCacheMap?: Map<string, string>
979
+ ): CallExpressionShape | TemplateExpressionShape | undefined {
980
+ // Find all step pill matches
981
+ const matches = Array.from(value.matchAll(STEP_PILL_REGEX))
982
+ if (matches.length === 0) {
983
+ return undefined
984
+ }
985
+
986
+ // Simple case: entire value is a single pill
987
+ const firstMatch = matches[0]
988
+ if (matches.length === 1 && firstMatch && firstMatch.index === 0 && firstMatch[0].length === value.length) {
989
+ const uuid = firstMatch[1]
990
+ const property = firstMatch[2]
991
+ if (uuid && property) {
992
+ return resolveStepPillMatch(uuid, property, source, cidToIdentifierMap, labelCacheMap)
993
+ }
994
+ return undefined
995
+ }
996
+
997
+ // Template case: mixed text + pills → TemplateExpressionShape
998
+ const spans: TemplateSpanShape[] = []
999
+ let head = ''
1000
+ let lastIndex = 0
1001
+
1002
+ for (let i = 0; i < matches.length; i++) {
1003
+ const match = matches[i]
1004
+ const uuid = match?.[1]
1005
+ const property = match?.[2]
1006
+ if (!match || !uuid || !property) {
1007
+ continue
1008
+ }
1009
+
1010
+ const pillExpr = resolveStepPillMatch(uuid, property, source, cidToIdentifierMap, labelCacheMap)
1011
+ if (!pillExpr) {
1012
+ continue
1013
+ }
1014
+
1015
+ const matchStart = match.index ?? 0
1016
+ const textBefore = value.substring(lastIndex, matchStart)
1017
+
1018
+ if (spans.length === 0) {
1019
+ head = textBefore
1020
+ }
1021
+
1022
+ // Find text after this pill (until next pill or end of string)
1023
+ const matchEnd = matchStart + match[0].length
1024
+ const nextMatch = matches[i + 1]
1025
+ const textAfter = nextMatch ? value.substring(matchEnd, nextMatch.index) : value.substring(matchEnd)
1026
+
1027
+ spans.push(
1028
+ new TemplateSpanShape({
1029
+ source,
1030
+ expression: pillExpr,
1031
+ literalText: textAfter,
1032
+ })
1033
+ )
1034
+
1035
+ lastIndex = matchEnd
1036
+ }
1037
+
1038
+ if (spans.length === 0) {
1039
+ return undefined
1040
+ }
1041
+
1042
+ return new TemplateExpressionShape({
1043
+ source,
1044
+ literalText: head,
1045
+ spans,
1046
+ })
1047
+ }
8
1048
 
9
1049
  const actionDefRelationships = {
10
1050
  sys_hub_step_instance: {
@@ -105,11 +1145,15 @@ const actionDefRelationships = {
105
1145
  },
106
1146
  },
107
1147
  },
1148
+ sys_hub_pill_compound: {
1149
+ via: 'attached_to',
1150
+ descendant: true,
1151
+ },
108
1152
  }
109
1153
 
110
1154
  export const ActionDefinitionPlugin = Plugin.create({
111
1155
  name: 'ActionDefinitionPlugin',
112
- docs: [createSdkDocEntry('ActionDefinition', ['sys_hub_action_type_definition'])],
1156
+ docs: [createSdkDocEntry('Action', ['sys_hub_action_type_definition'])],
113
1157
  records: {
114
1158
  sys_hub_action_type_definition: {
115
1159
  relationships: {
@@ -150,47 +1194,451 @@ export const ActionDefinitionPlugin = Plugin.create({
150
1194
  descendant: true,
151
1195
  },
152
1196
  },
153
- toShape(record, { logger }) {
154
- logger.warn(`Unsupported record type: ${record.getTable()}, Action definition is not implemented yet`)
155
- return { success: false }
156
- // TODO Uncomment this in next release
157
- // const inputs = buildVariableShapes(descendants.query('sys_hub_action_input'), descendants)
158
- // const outputs = buildVariableShapes(descendants.query('sys_hub_action_output'), descendants)
159
-
160
- // // Create the CallExpressionShape for the action definition
161
- // const callExpression = new CallExpressionShape({
162
- // source: record,
163
- // callee: 'ActionDefinition',
164
- // args: [
165
- // record.transform(({ $ }) => ({
166
- // $id: $.val(NowIdShape.from(record)),
167
- // name: $,
168
- // annotation: $.def(''),
169
- // description: $.def(''),
170
- // natlang: $.def(''),
171
- // access: $.def('public'),
172
- // category: $.def(''),
173
- // protection: $.from('sys_policy').def(''),
174
- // inputs: $.val(inputs),
175
- // outputs: $.val(outputs),
176
- // })),
177
- // ],
178
- // })
179
-
180
- // // Try to get the existing identifier from the record source, fallback to slugified name
181
- // const actionName =
182
- // getIdentifierFromRecord(record) ??
183
- // slugifyString(String(record.get('internal_name')?.getValue() || record.get('name')?.getValue()))
184
-
185
- // return {
186
- // success: true,
187
- // value: new VariableStatementShape({
188
- // source: record,
189
- // variableName: actionName,
190
- // initializer: callExpression,
191
- // isExported: true,
192
- // }),
193
- // }
1197
+ async toShape(record, { descendants, diagnostics, logger }) {
1198
+ const actionSysId = record.getId().getValue()
1199
+
1200
+ // Fall back to Record API for actions with error evaluation conditions (not yet fully supported).
1201
+ // We check for sys_hub_status_condition records (the actual conditions) rather than
1202
+ // sys_hub_action_status_metadata (which can exist as empty shells without conditions).
1203
+ const actionStatusMetadataIds = descendants
1204
+ .query('sys_hub_action_status_metadata')
1205
+ .filter((r) => r.get('action_type_id')?.asString()?.getValue() === actionSysId)
1206
+ .map((r) => r.getId().getValue())
1207
+ const hasErrorEvaluation = descendants
1208
+ .query('sys_hub_status_condition')
1209
+ .some((r) =>
1210
+ actionStatusMetadataIds.includes(
1211
+ r.get('action_status_metadata_id')?.asString()?.getValue() ?? ''
1212
+ )
1213
+ )
1214
+ if (hasErrorEvaluation) {
1215
+ const actionName = record.get('name')?.getValue() ?? actionSysId
1216
+ logger.warn(`Action '${actionName}' has error evaluation — falling back to Record API`)
1217
+ return { success: false }
1218
+ }
1219
+
1220
+ // Filter inputs/outputs to only those belonging to this action definition,
1221
+ // excluding snapshot-parented records
1222
+ const actionInputs = descendants
1223
+ .query('sys_hub_action_input')
1224
+ .filter((r) => r.get('model')?.asString()?.getValue() === actionSysId)
1225
+ const actionOutputs = descendants
1226
+ .query('sys_hub_action_output')
1227
+ .filter((r) => r.get('model')?.asString()?.getValue() === actionSysId)
1228
+
1229
+ // Fall back to Record API for custom actions with user-defined outputs (not yet fully supported).
1230
+ // Core actions (in CORE_ACTIONS_SYS_ID_NAME_MAP) are fully supported and should not fall back.
1231
+ // System-generated outputs (__action_status__, __dont_treat_as_error__) are excluded —
1232
+ // every action has these automatically, they are not custom outputs.
1233
+ const isCoreAction = actionSysId in CORE_ACTIONS_SYS_ID_NAME_MAP
1234
+ const customOutputs = actionOutputs.filter((r) => {
1235
+ const element = r.get('element')?.asString()?.getValue() ?? ''
1236
+ return element !== '__action_status__' && element !== '__dont_treat_as_error__'
1237
+ })
1238
+ if (!isCoreAction && customOutputs.length > 0) {
1239
+ const actionName = record.get('name')?.getValue() ?? actionSysId
1240
+ logger.warn(`Custom action '${actionName}' has outputs — falling back to Record API`)
1241
+ return { success: false }
1242
+ }
1243
+
1244
+ // Build snapshot output value map: element name → assigned value
1245
+ // Output values are stored in two places on the snapshot:
1246
+ // 1. sys_variable_value records (plain text values)
1247
+ // 2. sys_element_mapping records (datapill/template values)
1248
+ const snapshotOutputValues = new Map<string, string>()
1249
+ const snapshots = descendants.query('sys_hub_action_type_snapshot')
1250
+ if (snapshots.length > 0) {
1251
+ const snapshotId = snapshots[0]?.getId()?.getValue()
1252
+
1253
+ // Check sys_element_mapping for datapill values (field=element name, id=snapshot id)
1254
+ const elementMappings = descendants
1255
+ .query('sys_element_mapping')
1256
+ .filter((r) => r.get('id')?.asString()?.getValue() === snapshotId)
1257
+ for (const mapping of elementMappings) {
1258
+ const fieldName = mapping.get('field')?.asString()?.getValue()
1259
+ const value = mapping.get('value')?.ifString()?.getValue()
1260
+ if (fieldName && value !== undefined && value !== '') {
1261
+ snapshotOutputValues.set(fieldName, value)
1262
+ }
1263
+ }
1264
+
1265
+ // Check sys_variable_value for plain text values (on snapshot outputs)
1266
+ const snapshotOutputs = descendants
1267
+ .query('sys_hub_action_output')
1268
+ .filter((r) => r.get('model')?.asString()?.getValue() === snapshotId)
1269
+ for (const snapshotOutput of snapshotOutputs) {
1270
+ const elementName = snapshotOutput.get('element')?.asString()?.getValue()
1271
+ if (!elementName || snapshotOutputValues.has(elementName)) {
1272
+ continue
1273
+ }
1274
+ const snapshotOutputSysId = snapshotOutput.getId().getValue()
1275
+ const varValues = descendants
1276
+ .query('sys_variable_value')
1277
+ .filter((v) => v.get('variable')?.asString()?.getValue() === snapshotOutputSysId)
1278
+ if (varValues.length > 0) {
1279
+ const value = varValues[0]?.get('value')?.ifString()?.getValue()
1280
+ if (value !== undefined && value !== '') {
1281
+ snapshotOutputValues.set(elementName, value)
1282
+ }
1283
+ }
1284
+ }
1285
+ }
1286
+
1287
+ const inputs = buildVariableShapes(actionInputs, descendants)
1288
+ const outputs = buildVariableShapes(actionOutputs, descendants, undefined, snapshotOutputValues)
1289
+
1290
+ // Extract label_cache from action definition for datapill type info
1291
+ let labelCacheMap: Map<string, string> | undefined
1292
+ try {
1293
+ const labelCacheValue = record.get('label_cache')?.asString()?.getValue()
1294
+ if (labelCacheValue) {
1295
+ labelCacheMap = createLableCacheNameToTypeMap(labelCacheValue, logger)
1296
+ }
1297
+ } catch {
1298
+ labelCacheMap = undefined
1299
+ }
1300
+
1301
+ // Build step instance shapes for the action body
1302
+ // Filter to only step instances belonging to this action definition,
1303
+ // excluding snapshot-parented step instances
1304
+ const stepInstances = descendants
1305
+ .query('sys_hub_step_instance')
1306
+ .filter((s) => s.get('action')?.asString()?.getValue() === actionSysId)
1307
+ const stepShapes: Shape[] = []
1308
+ const cidToIdentifierMap = new Map<string, IdentifierShape>()
1309
+
1310
+ if (stepInstances.length > 0) {
1311
+ const sortedSteps = [...stepInstances].sort((a, b) => {
1312
+ const orderA = Number(a.get('order')?.getValue() ?? 0)
1313
+ const orderB = Number(b.get('order')?.getValue() ?? 0)
1314
+ return orderA - orderB
1315
+ })
1316
+
1317
+ const allVariableValues = descendants.query('sys_variable_value')
1318
+ const allElementMappings = descendants.query('sys_element_mapping')
1319
+ const usedVarNames = new Set<string>()
1320
+ const allInputScripts = descendants.query('sys_hub_input_scripts')
1321
+ const allExtInputs = descendants.query('sys_hub_step_ext_input')
1322
+ const allExtOutputs = descendants.query('sys_hub_step_ext_output')
1323
+ const allDocumentation = descendants.query('sys_documentation')
1324
+
1325
+ for (const stepInstance of sortedSteps) {
1326
+ const stepSysId = stepInstance.getId().getValue()
1327
+ const stepTypeSysId = stepInstance.get('step_type')?.asString()?.getValue()
1328
+ if (!stepTypeSysId) {
1329
+ continue
1330
+ }
1331
+
1332
+ const builtInDef = BUILT_IN_STEP_DEFINITIONS[stepTypeSysId]
1333
+ if (!builtInDef) {
1334
+ logger.warn(
1335
+ `Unsupported action step type ${stepTypeSysId} found, falling back to Record() api. Action = ${actionSysId}`
1336
+ )
1337
+ return { success: false }
1338
+ }
1339
+
1340
+ // Filter variable values for this step instance
1341
+ const stepVarValues = allVariableValues.filter(
1342
+ (v) => v.get('document_key')?.asString()?.getValue() === stepSysId
1343
+ )
1344
+
1345
+ // Build reverse lookup: element name → uiType
1346
+ const elementNameToUiType: globalThis.Record<string, string | undefined> = {}
1347
+ for (const entry of Object.values(builtInDef.inputs)) {
1348
+ elementNameToUiType[getVarEntryName(entry)] = getVarEntryType(entry)
1349
+ }
1350
+ for (const entry of Object.values(builtInDef.outputs)) {
1351
+ elementNameToUiType[getVarEntryName(entry)] = getVarEntryType(entry)
1352
+ }
1353
+
1354
+ const inputsObj: Props = {}
1355
+
1356
+ // Process sys_variable_value records (simple scalar values)
1357
+ for (const varValue of stepVarValues) {
1358
+ const variableSysId = varValue.get('variable')?.asString()?.getValue()
1359
+ const value = varValue.get('value')?.asString()?.getValue()
1360
+ if (variableSysId) {
1361
+ const entry = builtInDef.inputs[variableSysId] ?? builtInDef.outputs[variableSysId]
1362
+ const elementName = entry ? getVarEntryName(entry) : undefined
1363
+ const uiType = entry ? getVarEntryType(entry) : undefined
1364
+ if (elementName && value != null) {
1365
+ inputsObj[elementName] = uiType
1366
+ ? normalizeInputValue(value, uiType, stepInstance)
1367
+ : value
1368
+ }
1369
+ }
1370
+ }
1371
+
1372
+ // Process sys_element_mapping records (datapill/template values)
1373
+ // Build a lookup of ext input datapill values from element_mappings
1374
+ const extInputMappingValues: globalThis.Record<string, string> = {}
1375
+ const stepElementMappings = allElementMappings.filter(
1376
+ (m) => m.get('id')?.asString()?.getValue() === stepSysId
1377
+ )
1378
+ for (const mapping of stepElementMappings) {
1379
+ const rawFieldName = mapping.get('field')?.asString()?.getValue()
1380
+ const value = mapping.get('value')?.asString()?.getValue()
1381
+ const table = mapping.get('table')?.asString()?.getValue() ?? ''
1382
+ if (!rawFieldName || !value) {
1383
+ continue
1384
+ }
1385
+ // Ext input element_mappings have table var__m_sys_hub_step_ext_input_*
1386
+ // Collect their values but don't add to inputsObj (they go under inputVariables)
1387
+ if (table.startsWith('var__m_sys_hub_step_ext_input_')) {
1388
+ extInputMappingValues[rawFieldName] = value
1389
+ continue
1390
+ }
1391
+ // Translate platform-internal field names (e.g. __snc_dont_fail_on_error)
1392
+ // to their Fluent API names (e.g. dont_fail_flow_on_error)
1393
+ const fieldName = ELEMENT_MAPPING_FIELD_ALIASES[rawFieldName] ?? rawFieldName
1394
+ const uiType = elementNameToUiType[fieldName]
1395
+ inputsObj[fieldName] = uiType ? normalizeInputValue(value, uiType, stepInstance) : value
1396
+ }
1397
+
1398
+ // Default platform-omitted boolean/choice inputs when the step definition supports them
1399
+ const allElementNames = Object.values(builtInDef.inputs).map(getVarEntryName)
1400
+ if (allElementNames.includes('sort_type') && inputsObj['sort_type'] === undefined) {
1401
+ inputsObj['sort_type'] = 'sort_asc'
1402
+ }
1403
+ if (allElementNames.includes('set_workflow') && inputsObj['set_workflow'] === undefined) {
1404
+ inputsObj['set_workflow'] = true
1405
+ }
1406
+ if (
1407
+ allElementNames.includes('set_autosysfields') &&
1408
+ inputsObj['set_autosysfields'] === undefined
1409
+ ) {
1410
+ inputsObj['set_autosysfields'] = true
1411
+ }
1412
+ if (allElementNames.includes('log_level') && inputsObj['log_level'] === undefined) {
1413
+ inputsObj['log_level'] = 'info'
1414
+ }
1415
+
1416
+ // Map error_handling_type number to Fluent string
1417
+ const errorHandlingRaw = stepInstance.get('error_handling_type')
1418
+ const errorHandlingType =
1419
+ errorHandlingRaw?.ifNumber()?.getValue() ?? Number(errorHandlingRaw?.ifString()?.getValue())
1420
+ inputsObj['errorHandlingType'] =
1421
+ errorHandlingType === 2 ? 'dont_stop_the_action' : 'stop_the_action'
1422
+
1423
+ // For script steps, ensure required_run_time is set (default to 'instance' if missing)
1424
+ if (
1425
+ BUILT_IN_STEP_SYS_ID_NAME_MAP[stepTypeSysId] === 'script' &&
1426
+ !inputsObj['required_run_time']
1427
+ ) {
1428
+ inputsObj['required_run_time'] = 'instance'
1429
+ }
1430
+
1431
+ // Resolve inline scripts: replace fd-scripted placeholders with wfa.inlineScript()
1432
+ resolveInlineScripts(inputsObj, stepSysId, allInputScripts, stepInstance)
1433
+
1434
+ // Externalize script field to a separate .js file via Now.include()
1435
+ if (typeof inputsObj['script'] === 'string' && inputsObj['script']) {
1436
+ // Strip &#13; (XML carriage return entities) that the platform embeds in script values
1437
+ const cleanScript = (inputsObj['script'] as string).replace(/&#13;/g, '')
1438
+ inputsObj['script'] = new NowIncludeShape({
1439
+ source: stepInstance,
1440
+ path: `./scripts/${stepSysId}.js`,
1441
+ includedText: cleanScript,
1442
+ })
1443
+ }
1444
+
1445
+ // Build inputVariables from sys_hub_step_ext_input records
1446
+ const stepExtInputs = allExtInputs.filter(
1447
+ (r) => r.get('model_id')?.asString()?.getValue() === stepSysId
1448
+ )
1449
+ if (stepExtInputs.length > 0) {
1450
+ const inputVariablesObj: Props = {}
1451
+ for (const extInput of stepExtInputs) {
1452
+ const elementName = extInput.get('element')?.asString()?.getValue()
1453
+ if (!elementName) {
1454
+ continue
1455
+ }
1456
+ const extInputSysId = extInput.getId().getValue()
1457
+ const label = extInput.get('label')?.asString()?.getValue() || elementName
1458
+ // Find value from sys_variable_value or element_mapping (datapill)
1459
+ // Prioritize pills from sys_element_mapping over static values from sys_variable_value
1460
+ const varValueRecord = stepVarValues.find(
1461
+ (v) => v.get('variable')?.asString()?.getValue() === extInputSysId
1462
+ )
1463
+ const value =
1464
+ extInputMappingValues[elementName] ||
1465
+ varValueRecord?.get('value')?.asString()?.getValue() ||
1466
+ ''
1467
+ const inputEntry: Props = { label }
1468
+ if (value) {
1469
+ inputEntry['value'] = value
1470
+ }
1471
+ const inputDefault = extInput.get('default_value')?.asString()?.getValue()
1472
+ if (inputDefault) {
1473
+ inputEntry['defaultValue'] = inputDefault
1474
+ }
1475
+ inputVariablesObj[elementName] = inputEntry
1476
+ }
1477
+ inputsObj['inputVariables'] = inputVariablesObj
1478
+ }
1479
+
1480
+ // Build outputVariables from sys_hub_step_ext_output records
1481
+ const stepExtOutputs = allExtOutputs.filter(
1482
+ (r) => r.get('model_id')?.asString()?.getValue() === stepSysId
1483
+ )
1484
+ if (stepExtOutputs.length > 0) {
1485
+ const outputVariablesObj: Props = {}
1486
+ for (const extOutput of stepExtOutputs) {
1487
+ const elementName = extOutput.get('element')?.asString()?.getValue()
1488
+ if (!elementName) {
1489
+ continue
1490
+ }
1491
+ const label = extOutput.get('label')?.asString()?.getValue() || elementName
1492
+ const mandatory = extOutput.get('mandatory')?.asString()?.getValue() === 'true'
1493
+ const internalType = extOutput.get('internal_type')?.asString()?.getValue() ?? 'string'
1494
+ // Check attributes for uiType to handle cases where internal_type is generic
1495
+ const attributes = extOutput.get('attributes')?.asString()?.getValue() ?? ''
1496
+ const uiType = getAttributeValue(attributes, 'uiType') ?? internalType
1497
+ const columnApiName = COLUMN_TYPE_TO_API[uiType] ?? 'StringColumn'
1498
+ const columnProps: Props = { label }
1499
+ if (mandatory) {
1500
+ columnProps['mandatory'] = true
1501
+ }
1502
+ const maxLength = extOutput.get('max_length')?.asString()?.getValue()
1503
+ if (maxLength) {
1504
+ columnProps['maxLength'] = Number(maxLength)
1505
+ }
1506
+ // Hint is stored in sys_documentation, not on the ext_output record
1507
+ const extOutputName = extOutput.get('name')?.asString()?.getValue()
1508
+ const docRecord = allDocumentation.find(
1509
+ (d) =>
1510
+ d.get('name')?.asString()?.getValue() === extOutputName &&
1511
+ d.get('element')?.asString()?.getValue() === elementName
1512
+ )
1513
+ const hint = docRecord?.get('hint')?.asString()?.getValue()
1514
+ if (hint) {
1515
+ columnProps['hint'] = hint
1516
+ }
1517
+ const referenceTable = extOutput.get('reference')?.asString()?.getValue()
1518
+ if (
1519
+ referenceTable &&
1520
+ (columnApiName === 'ReferenceColumn' || columnApiName === 'ListColumn')
1521
+ ) {
1522
+ columnProps['referenceTable'] = referenceTable
1523
+ }
1524
+ const defaultValue = extOutput.get('default_value')?.asString()?.getValue()
1525
+ if (defaultValue) {
1526
+ columnProps['default'] = defaultValue
1527
+ }
1528
+ outputVariablesObj[elementName] = new CallExpressionShape({
1529
+ source: extOutput,
1530
+ callee: columnApiName,
1531
+ args: [
1532
+ new ObjectShape({
1533
+ source: extOutput,
1534
+ properties: columnProps,
1535
+ }),
1536
+ ],
1537
+ })
1538
+ }
1539
+ inputsObj['outputVariables'] = outputVariablesObj
1540
+ }
1541
+
1542
+ // Convert action input pills ({{action.xxx}}) to wfa.dataPill(params.inputs.xxx, 'type')
1543
+ convertActionPillsInInputs(inputsObj, stepInstance, diagnostics, labelCacheMap)
1544
+
1545
+ // Convert step output pills ({{step[UUID].xxx}}) to wfa.dataPill(varName.xxx, 'type')
1546
+ // Earlier steps are already in cidToIdentifierMap since steps are processed in order
1547
+ convertStepPillsInInputs(inputsObj, stepInstance, cidToIdentifierMap, labelCacheMap)
1548
+
1549
+ const configProperties: Props = {
1550
+ $id: NowIdShape.from(stepInstance),
1551
+ }
1552
+ const label = stepInstance.get('label')?.asString()?.getValue()
1553
+ if (label && label !== builtInDef.name) {
1554
+ configProperties['label'] = label
1555
+ }
1556
+
1557
+ // Built-in/OOB step: emit actionStep.xxx reference
1558
+ const stepIdentifier = getBuiltInStepIdentifier(stepTypeSysId)
1559
+ // Built-in/OOB step: emit actionStep.xxx identifier reference
1560
+ const stepDefShape = stepIdentifier
1561
+ ? new IdentifierShape({ source: stepInstance, name: stepIdentifier })
1562
+ : new IdentifierShape({ source: stepInstance, name: builtInDef.name })
1563
+
1564
+ // Map CID → variable identifier for step datapill resolution in later steps
1565
+ const cid = stepInstance.get('cid')?.asString()?.getValue()
1566
+ const varName = generateUniqueVarName(label || builtInDef.name, usedVarNames)
1567
+ usedVarNames.add(varName)
1568
+ if (cid) {
1569
+ cidToIdentifierMap.set(cid, new IdentifierShape({ source: stepInstance, name: varName }))
1570
+ }
1571
+
1572
+ const stepCallExpr = new CallExpressionShape({
1573
+ source: stepInstance,
1574
+ callee: 'wfa.actionStep',
1575
+ args: [
1576
+ stepDefShape,
1577
+ new ObjectShape({
1578
+ source: stepInstance,
1579
+ properties: configProperties,
1580
+ }),
1581
+ new ObjectShape({
1582
+ source: stepInstance,
1583
+ properties: inputsObj,
1584
+ }),
1585
+ ],
1586
+ })
1587
+
1588
+ stepShapes.push(
1589
+ new VariableStatementShape({
1590
+ source: stepInstance,
1591
+ variableName: varName,
1592
+ initializer: stepCallExpr,
1593
+ })
1594
+ )
1595
+ }
1596
+ }
1597
+
1598
+ // Build args: config + optional body with step instances
1599
+ const actionConfig = record.transform(({ $ }) => ({
1600
+ $id: $.val(NowIdShape.from(record)),
1601
+ name: $,
1602
+ annotation: $.def(''),
1603
+ description: $.def(''),
1604
+ access: $.def('public'),
1605
+ category: $.def(''),
1606
+ protection: $.from('sys_policy').def(''),
1607
+ inputs: $.val(inputs),
1608
+ outputs: $.val(outputs),
1609
+ }))
1610
+
1611
+ const args: unknown[] = [actionConfig]
1612
+ if (stepShapes.length > 0) {
1613
+ args.push(
1614
+ new ArrowFunctionShape({
1615
+ source: record,
1616
+ parameters: [ACTION_PILL_PARAM_NAME],
1617
+ statements: stepShapes,
1618
+ })
1619
+ )
1620
+ }
1621
+
1622
+ const callExpression = new CallExpressionShape({
1623
+ source: record,
1624
+ callee: 'Action',
1625
+ args,
1626
+ })
1627
+
1628
+ // Try to get the existing identifier from the record source, fallback to slugified name
1629
+ const actionName =
1630
+ getIdentifierFromRecord(record) ??
1631
+ slugifyString(String(record.get('internal_name')?.getValue() || record.get('name')?.getValue()))
1632
+
1633
+ return {
1634
+ success: true,
1635
+ value: new VariableStatementShape({
1636
+ source: record,
1637
+ variableName: actionName,
1638
+ initializer: callExpression,
1639
+ isExported: true,
1640
+ }),
1641
+ }
194
1642
  },
195
1643
  async diff(existing, incoming, descendants, context) {
196
1644
  return deleteMultipleDiff(existing, incoming, descendants, context)
@@ -211,18 +1659,28 @@ export const ActionDefinitionPlugin = Plugin.create({
211
1659
  sys_complex_object: {
212
1660
  coalesce: ['name'],
213
1661
  },
1662
+ sys_hub_action_status_metadata: {
1663
+ coalesce: ['action_type_id'],
1664
+ },
1665
+ sys_hub_status_condition: {
1666
+ coalesce: ['action_status_metadata_id', 'order'],
1667
+ },
214
1668
  },
215
1669
  shapes: [
216
1670
  {
217
1671
  shape: CallExpressionShape,
218
1672
  fileTypes: ['fluent'],
219
- async toRecord(callExpression, { factory, diagnostics }) {
220
- if (callExpression.getCallee() !== 'ActionDefinition') {
1673
+ async toRecord(callExpression, { factory, diagnostics, transform }) {
1674
+ const callee = callExpression.getCallee()
1675
+ if (callee !== 'Action') {
221
1676
  return { success: false }
222
1677
  }
223
1678
  const relatedRecords: Record[] = []
224
1679
  const actionConfiguration = callExpression.getArgument(0).asObject()
225
- const actionDefinitionRecord = await factory.createRecord({
1680
+
1681
+ const inputsConfig = actionConfiguration.get('inputs').ifObject()?.asObject()
1682
+
1683
+ let actionDefinitionRecord = await factory.createRecord({
226
1684
  source: callExpression,
227
1685
  table: 'sys_hub_action_type_definition',
228
1686
  explicitId: actionConfiguration.get('$id'),
@@ -240,7 +1698,6 @@ export const ActionDefinitionPlugin = Plugin.create({
240
1698
  })),
241
1699
  })
242
1700
 
243
- const inputsConfig = actionConfiguration.get('inputs').ifObject()?.asObject()
244
1701
  if (inputsConfig) {
245
1702
  const { variableRecords: inputRecords, complexObjectRecords: inputComplexObjects } =
246
1703
  await buildVariableRecords({
@@ -270,20 +1727,83 @@ export const ActionDefinitionPlugin = Plugin.create({
270
1727
  relatedRecords.push(...outputRecords, ...outputComplexObjects)
271
1728
  }
272
1729
 
273
- let actionBody: ArrowFunctionShape | undefined
274
- actionBody = !callExpression.getArgument(1)?.if(UndefinedShape)
275
- ? callExpression.getArgument(1)?.as(ArrowFunctionShape)
276
- : undefined
1730
+ // arg[1] is the optional body (ArrowFunction) containing step instances
1731
+ const arg1 = callExpression.getArgument(1)
1732
+ const actionBody: ArrowFunctionShape | undefined =
1733
+ arg1 && !arg1.if(UndefinedShape) ? arg1.as(ArrowFunctionShape) : undefined
277
1734
  const allInstances = actionBody?.getStatements()
278
1735
 
1736
+ const cidMap = new Map<string, string>()
1737
+ const cidToLabelMap = new Map<string, string>()
1738
+ const pillTypeMap = new Map<string, string>()
1739
+
1740
+ const stepInfos: StepInfo[] = []
1741
+ let order = 1
279
1742
  for (const [, v] of (allInstances ?? []).entries()) {
280
- const source = v.getSource()
281
- if (source instanceof StepInstanceShape) {
282
- const instanceRecord = v.as(Record)
283
- const record = instanceRecord.merge({
284
- action: actionDefinitionRecord.getId().getValue(),
285
- })
286
- relatedRecords.push(record)
1743
+ const isVarStatement = v instanceof VariableStatementShape
1744
+ const innerShape = isVarStatement ? v.getInitializer() : v
1745
+
1746
+ if (innerShape.getSource() instanceof StepInstanceShape) {
1747
+ const stepShape = innerShape.getSource() as StepInstanceShape
1748
+ stepShape.setCidMap(cidMap)
1749
+ stepShape.setPillTypeMap(pillTypeMap)
1750
+
1751
+ const stepResult = await transform.toRecord(innerShape, StepInstancePlugin)
1752
+ if (stepResult.success) {
1753
+ const record = stepResult.value.merge({
1754
+ action: actionDefinitionRecord.getId().getValue(),
1755
+ order: order,
1756
+ })
1757
+ relatedRecords.push(record)
1758
+ stepInfos.push({
1759
+ shape: stepShape,
1760
+ stepInstanceSysId: record.getId().getValue(),
1761
+ stepDefinitionSysId: stepShape.getStepDefinitionSysId(),
1762
+ })
1763
+
1764
+ if (isVarStatement) {
1765
+ const varName = (v as VariableStatementShape).getVariableName().getName()
1766
+ const stepCid = record.get('cid')?.asString()?.getValue() ?? ''
1767
+ const stepLabel = record.get('label')?.asString()?.getValue() || varName || 'Step'
1768
+ if (varName && stepCid) {
1769
+ cidMap.set(varName, stepCid)
1770
+ cidToLabelMap.set(stepCid, stepLabel)
1771
+ }
1772
+ }
1773
+ order++
1774
+ } else if (isVarStatement) {
1775
+ // Step processing failed - log diagnostic to help debug cascading failures
1776
+ const varName = (v as VariableStatementShape).getVariableName().getName()
1777
+ diagnostics.error(
1778
+ innerShape,
1779
+ `Step '${varName}' failed to process. Subsequent steps referencing this step's outputs may not resolve correctly.`
1780
+ )
1781
+ }
1782
+ }
1783
+ }
1784
+
1785
+ // Re-resolve step pills that couldn't be resolved during auto-processing
1786
+ // (cidMap was empty). Now cidMap is fully populated — walk StepInstanceShape
1787
+ // inputs directly and create correct sys_element_mapping records.
1788
+ const resolvedStepPillRecords = await resolveUnresolvedStepPills(
1789
+ stepInfos,
1790
+ cidMap,
1791
+ pillTypeMap,
1792
+ factory
1793
+ )
1794
+ relatedRecords.push(...resolvedStepPillRecords)
1795
+
1796
+ const collectedStepPills = extractStepPillsFromRecords(relatedRecords, cidToLabelMap, pillTypeMap)
1797
+ const dotWalkPills = extractActionDotWalkPills(relatedRecords, pillTypeMap)
1798
+
1799
+ if (inputsConfig) {
1800
+ const labelCacheJson = buildActionLabelCache(inputsConfig, {
1801
+ stepPills: collectedStepPills,
1802
+ dotWalkPills,
1803
+ cidToLabelMap,
1804
+ })
1805
+ if (labelCacheJson) {
1806
+ actionDefinitionRecord = actionDefinitionRecord.merge({ label_cache: labelCacheJson })
287
1807
  }
288
1808
  }
289
1809