@elizaos/plugin-workflow 2.0.0-beta.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 (294) hide show
  1. package/README.md +71 -0
  2. package/auto-enable.ts +18 -0
  3. package/dist/actions/index.d.ts +2 -0
  4. package/dist/actions/index.d.ts.map +1 -0
  5. package/dist/actions/index.js +2 -0
  6. package/dist/actions/index.js.map +1 -0
  7. package/dist/actions/workflow.d.ts +23 -0
  8. package/dist/actions/workflow.d.ts.map +1 -0
  9. package/dist/actions/workflow.js +425 -0
  10. package/dist/actions/workflow.js.map +1 -0
  11. package/dist/data/defaultNodes.json +9887 -0
  12. package/dist/data/schemaIndex.json +1 -0
  13. package/dist/data/triggerSchemaIndex.json +1 -0
  14. package/dist/db/index.d.ts +2 -0
  15. package/dist/db/index.d.ts.map +1 -0
  16. package/dist/db/index.js +2 -0
  17. package/dist/db/index.js.map +1 -0
  18. package/dist/db/schema.d.ts +588 -0
  19. package/dist/db/schema.d.ts.map +1 -0
  20. package/dist/db/schema.js +59 -0
  21. package/dist/db/schema.js.map +1 -0
  22. package/dist/index.d.ts +34 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +126 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/lib/automations-builder.d.ts +21 -0
  27. package/dist/lib/automations-builder.d.ts.map +1 -0
  28. package/dist/lib/automations-builder.js +557 -0
  29. package/dist/lib/automations-builder.js.map +1 -0
  30. package/dist/lib/automations-types.d.ts +153 -0
  31. package/dist/lib/automations-types.d.ts.map +1 -0
  32. package/dist/lib/automations-types.js +191 -0
  33. package/dist/lib/automations-types.js.map +1 -0
  34. package/dist/lib/index.d.ts +3 -0
  35. package/dist/lib/index.d.ts.map +1 -0
  36. package/dist/lib/index.js +3 -0
  37. package/dist/lib/index.js.map +1 -0
  38. package/dist/lib/legacy-task-migration.d.ts +20 -0
  39. package/dist/lib/legacy-task-migration.d.ts.map +1 -0
  40. package/dist/lib/legacy-task-migration.js +110 -0
  41. package/dist/lib/legacy-task-migration.js.map +1 -0
  42. package/dist/lib/legacy-text-trigger-migration.d.ts +18 -0
  43. package/dist/lib/legacy-text-trigger-migration.d.ts.map +1 -0
  44. package/dist/lib/legacy-text-trigger-migration.js +131 -0
  45. package/dist/lib/legacy-text-trigger-migration.js.map +1 -0
  46. package/dist/lib/workflow-clarification.d.ts +113 -0
  47. package/dist/lib/workflow-clarification.d.ts.map +1 -0
  48. package/dist/lib/workflow-clarification.js +425 -0
  49. package/dist/lib/workflow-clarification.js.map +1 -0
  50. package/dist/plugin-routes.d.ts +9 -0
  51. package/dist/plugin-routes.d.ts.map +1 -0
  52. package/dist/plugin-routes.js +147 -0
  53. package/dist/plugin-routes.js.map +1 -0
  54. package/dist/providers/activeWorkflows.d.ts +11 -0
  55. package/dist/providers/activeWorkflows.d.ts.map +1 -0
  56. package/dist/providers/activeWorkflows.js +72 -0
  57. package/dist/providers/activeWorkflows.js.map +1 -0
  58. package/dist/providers/index.d.ts +4 -0
  59. package/dist/providers/index.d.ts.map +1 -0
  60. package/dist/providers/index.js +4 -0
  61. package/dist/providers/index.js.map +1 -0
  62. package/dist/providers/pendingDraft.d.ts +9 -0
  63. package/dist/providers/pendingDraft.d.ts.map +1 -0
  64. package/dist/providers/pendingDraft.js +48 -0
  65. package/dist/providers/pendingDraft.js.map +1 -0
  66. package/dist/providers/workflowStatus.d.ts +3 -0
  67. package/dist/providers/workflowStatus.d.ts.map +1 -0
  68. package/dist/providers/workflowStatus.js +69 -0
  69. package/dist/providers/workflowStatus.js.map +1 -0
  70. package/dist/register-routes.d.ts +2 -0
  71. package/dist/register-routes.d.ts.map +1 -0
  72. package/dist/register-routes.js +6 -0
  73. package/dist/register-routes.js.map +1 -0
  74. package/dist/routes/_helpers.d.ts +11 -0
  75. package/dist/routes/_helpers.d.ts.map +1 -0
  76. package/dist/routes/_helpers.js +22 -0
  77. package/dist/routes/_helpers.js.map +1 -0
  78. package/dist/routes/automations.d.ts +19 -0
  79. package/dist/routes/automations.d.ts.map +1 -0
  80. package/dist/routes/automations.js +32 -0
  81. package/dist/routes/automations.js.map +1 -0
  82. package/dist/routes/embedded-webhooks.d.ts +3 -0
  83. package/dist/routes/embedded-webhooks.d.ts.map +1 -0
  84. package/dist/routes/embedded-webhooks.js +47 -0
  85. package/dist/routes/embedded-webhooks.js.map +1 -0
  86. package/dist/routes/executions.d.ts +3 -0
  87. package/dist/routes/executions.d.ts.map +1 -0
  88. package/dist/routes/executions.js +58 -0
  89. package/dist/routes/executions.js.map +1 -0
  90. package/dist/routes/index.d.ts +4 -0
  91. package/dist/routes/index.d.ts.map +1 -0
  92. package/dist/routes/index.js +14 -0
  93. package/dist/routes/index.js.map +1 -0
  94. package/dist/routes/nodes.d.ts +3 -0
  95. package/dist/routes/nodes.d.ts.map +1 -0
  96. package/dist/routes/nodes.js +168 -0
  97. package/dist/routes/nodes.js.map +1 -0
  98. package/dist/routes/validation.d.ts +3 -0
  99. package/dist/routes/validation.d.ts.map +1 -0
  100. package/dist/routes/validation.js +41 -0
  101. package/dist/routes/validation.js.map +1 -0
  102. package/dist/routes/workflow-routes.d.ts +27 -0
  103. package/dist/routes/workflow-routes.d.ts.map +1 -0
  104. package/dist/routes/workflow-routes.js +326 -0
  105. package/dist/routes/workflow-routes.js.map +1 -0
  106. package/dist/routes/workflows.d.ts +3 -0
  107. package/dist/routes/workflows.d.ts.map +1 -0
  108. package/dist/routes/workflows.js +252 -0
  109. package/dist/routes/workflows.js.map +1 -0
  110. package/dist/schemas/draftIntent.d.ts +22 -0
  111. package/dist/schemas/draftIntent.d.ts.map +1 -0
  112. package/dist/schemas/draftIntent.js +22 -0
  113. package/dist/schemas/draftIntent.js.map +1 -0
  114. package/dist/schemas/feasibility.d.ts +13 -0
  115. package/dist/schemas/feasibility.d.ts.map +1 -0
  116. package/dist/schemas/feasibility.js +9 -0
  117. package/dist/schemas/feasibility.js.map +1 -0
  118. package/dist/schemas/index.d.ts +5 -0
  119. package/dist/schemas/index.d.ts.map +1 -0
  120. package/dist/schemas/index.js +5 -0
  121. package/dist/schemas/index.js.map +1 -0
  122. package/dist/schemas/keywordExtraction.d.ts +14 -0
  123. package/dist/schemas/keywordExtraction.d.ts.map +1 -0
  124. package/dist/schemas/keywordExtraction.js +12 -0
  125. package/dist/schemas/keywordExtraction.js.map +1 -0
  126. package/dist/schemas/workflowMatching.d.ts +36 -0
  127. package/dist/schemas/workflowMatching.d.ts.map +1 -0
  128. package/dist/schemas/workflowMatching.js +30 -0
  129. package/dist/schemas/workflowMatching.js.map +1 -0
  130. package/dist/services/embedded-workflow-service.d.ts +106 -0
  131. package/dist/services/embedded-workflow-service.d.ts.map +1 -0
  132. package/dist/services/embedded-workflow-service.js +1900 -0
  133. package/dist/services/embedded-workflow-service.js.map +1 -0
  134. package/dist/services/index.d.ts +5 -0
  135. package/dist/services/index.d.ts.map +1 -0
  136. package/dist/services/index.js +5 -0
  137. package/dist/services/index.js.map +1 -0
  138. package/dist/services/workflow-credential-store.d.ts +27 -0
  139. package/dist/services/workflow-credential-store.d.ts.map +1 -0
  140. package/dist/services/workflow-credential-store.js +92 -0
  141. package/dist/services/workflow-credential-store.js.map +1 -0
  142. package/dist/services/workflow-dispatch.d.ts +41 -0
  143. package/dist/services/workflow-dispatch.d.ts.map +1 -0
  144. package/dist/services/workflow-dispatch.js +86 -0
  145. package/dist/services/workflow-dispatch.js.map +1 -0
  146. package/dist/services/workflow-service.d.ts +63 -0
  147. package/dist/services/workflow-service.d.ts.map +1 -0
  148. package/dist/services/workflow-service.js +492 -0
  149. package/dist/services/workflow-service.js.map +1 -0
  150. package/dist/trigger-routes.d.ts +153 -0
  151. package/dist/trigger-routes.d.ts.map +1 -0
  152. package/dist/trigger-routes.js +424 -0
  153. package/dist/trigger-routes.js.map +1 -0
  154. package/dist/types/index.d.ts +457 -0
  155. package/dist/types/index.d.ts.map +1 -0
  156. package/dist/types/index.js +59 -0
  157. package/dist/types/index.js.map +1 -0
  158. package/dist/utils/catalog.d.ts +16 -0
  159. package/dist/utils/catalog.d.ts.map +1 -0
  160. package/dist/utils/catalog.js +211 -0
  161. package/dist/utils/catalog.js.map +1 -0
  162. package/dist/utils/clarification.d.ts +17 -0
  163. package/dist/utils/clarification.d.ts.map +1 -0
  164. package/dist/utils/clarification.js +46 -0
  165. package/dist/utils/clarification.js.map +1 -0
  166. package/dist/utils/context.d.ts +4 -0
  167. package/dist/utils/context.d.ts.map +1 -0
  168. package/dist/utils/context.js +18 -0
  169. package/dist/utils/context.js.map +1 -0
  170. package/dist/utils/credentialResolver.d.ts +22 -0
  171. package/dist/utils/credentialResolver.d.ts.map +1 -0
  172. package/dist/utils/credentialResolver.js +146 -0
  173. package/dist/utils/credentialResolver.js.map +1 -0
  174. package/dist/utils/generation.d.ts +36 -0
  175. package/dist/utils/generation.d.ts.map +1 -0
  176. package/dist/utils/generation.js +701 -0
  177. package/dist/utils/generation.js.map +1 -0
  178. package/dist/utils/host-capabilities.d.ts +27 -0
  179. package/dist/utils/host-capabilities.d.ts.map +1 -0
  180. package/dist/utils/host-capabilities.js +59 -0
  181. package/dist/utils/host-capabilities.js.map +1 -0
  182. package/dist/utils/inferSyntheticOutputSchema.d.ts +20 -0
  183. package/dist/utils/inferSyntheticOutputSchema.d.ts.map +1 -0
  184. package/dist/utils/inferSyntheticOutputSchema.js +151 -0
  185. package/dist/utils/inferSyntheticOutputSchema.js.map +1 -0
  186. package/dist/utils/outputSchema.d.ts +26 -0
  187. package/dist/utils/outputSchema.d.ts.map +1 -0
  188. package/dist/utils/outputSchema.js +297 -0
  189. package/dist/utils/outputSchema.js.map +1 -0
  190. package/dist/utils/validateAndRepair.d.ts +41 -0
  191. package/dist/utils/validateAndRepair.d.ts.map +1 -0
  192. package/dist/utils/validateAndRepair.js +483 -0
  193. package/dist/utils/validateAndRepair.js.map +1 -0
  194. package/dist/utils/workflow-prompts/actionResponse.d.ts +2 -0
  195. package/dist/utils/workflow-prompts/actionResponse.d.ts.map +1 -0
  196. package/dist/utils/workflow-prompts/actionResponse.js +17 -0
  197. package/dist/utils/workflow-prompts/actionResponse.js.map +1 -0
  198. package/dist/utils/workflow-prompts/draftIntent.d.ts +2 -0
  199. package/dist/utils/workflow-prompts/draftIntent.d.ts.map +1 -0
  200. package/dist/utils/workflow-prompts/draftIntent.js +23 -0
  201. package/dist/utils/workflow-prompts/draftIntent.js.map +1 -0
  202. package/dist/utils/workflow-prompts/feasibilityCheck.d.ts +2 -0
  203. package/dist/utils/workflow-prompts/feasibilityCheck.d.ts.map +1 -0
  204. package/dist/utils/workflow-prompts/feasibilityCheck.js +21 -0
  205. package/dist/utils/workflow-prompts/feasibilityCheck.js.map +1 -0
  206. package/dist/utils/workflow-prompts/fieldCorrection.d.ts +3 -0
  207. package/dist/utils/workflow-prompts/fieldCorrection.d.ts.map +1 -0
  208. package/dist/utils/workflow-prompts/fieldCorrection.js +20 -0
  209. package/dist/utils/workflow-prompts/fieldCorrection.js.map +1 -0
  210. package/dist/utils/workflow-prompts/index.d.ts +8 -0
  211. package/dist/utils/workflow-prompts/index.d.ts.map +1 -0
  212. package/dist/utils/workflow-prompts/index.js +8 -0
  213. package/dist/utils/workflow-prompts/index.js.map +1 -0
  214. package/dist/utils/workflow-prompts/keywordExtraction.d.ts +2 -0
  215. package/dist/utils/workflow-prompts/keywordExtraction.d.ts.map +1 -0
  216. package/dist/utils/workflow-prompts/keywordExtraction.js +21 -0
  217. package/dist/utils/workflow-prompts/keywordExtraction.js.map +1 -0
  218. package/dist/utils/workflow-prompts/parameterCorrection.d.ts +3 -0
  219. package/dist/utils/workflow-prompts/parameterCorrection.d.ts.map +1 -0
  220. package/dist/utils/workflow-prompts/parameterCorrection.js +29 -0
  221. package/dist/utils/workflow-prompts/parameterCorrection.js.map +1 -0
  222. package/dist/utils/workflow-prompts/workflowGeneration.d.ts +2 -0
  223. package/dist/utils/workflow-prompts/workflowGeneration.d.ts.map +1 -0
  224. package/dist/utils/workflow-prompts/workflowGeneration.js +529 -0
  225. package/dist/utils/workflow-prompts/workflowGeneration.js.map +1 -0
  226. package/dist/utils/workflow-prompts/workflowMatching.d.ts +2 -0
  227. package/dist/utils/workflow-prompts/workflowMatching.d.ts.map +1 -0
  228. package/dist/utils/workflow-prompts/workflowMatching.js +23 -0
  229. package/dist/utils/workflow-prompts/workflowMatching.js.map +1 -0
  230. package/dist/utils/workflow.d.ts +62 -0
  231. package/dist/utils/workflow.d.ts.map +1 -0
  232. package/dist/utils/workflow.js +712 -0
  233. package/dist/utils/workflow.js.map +1 -0
  234. package/package.json +87 -0
  235. package/src/actions/index.ts +1 -0
  236. package/src/actions/workflow.ts +494 -0
  237. package/src/data/defaultNodes.json +9887 -0
  238. package/src/data/schemaIndex.json +1 -0
  239. package/src/data/triggerSchemaIndex.json +1 -0
  240. package/src/db/index.ts +8 -0
  241. package/src/db/schema.ts +94 -0
  242. package/src/index.ts +179 -0
  243. package/src/lib/automations-builder.ts +679 -0
  244. package/src/lib/automations-types.ts +391 -0
  245. package/src/lib/index.ts +8 -0
  246. package/src/lib/legacy-task-migration.ts +143 -0
  247. package/src/lib/legacy-text-trigger-migration.ts +178 -0
  248. package/src/lib/workflow-clarification.ts +497 -0
  249. package/src/plugin-routes.ts +164 -0
  250. package/src/providers/activeWorkflows.ts +81 -0
  251. package/src/providers/index.ts +3 -0
  252. package/src/providers/pendingDraft.ts +55 -0
  253. package/src/providers/workflowStatus.ts +88 -0
  254. package/src/register-routes.ts +6 -0
  255. package/src/routes/_helpers.ts +27 -0
  256. package/src/routes/automations.ts +46 -0
  257. package/src/routes/embedded-webhooks.ts +64 -0
  258. package/src/routes/executions.ts +75 -0
  259. package/src/routes/index.ts +16 -0
  260. package/src/routes/nodes.ts +211 -0
  261. package/src/routes/validation.ts +51 -0
  262. package/src/routes/workflow-routes.ts +469 -0
  263. package/src/routes/workflows.ts +310 -0
  264. package/src/schemas/draftIntent.ts +21 -0
  265. package/src/schemas/feasibility.ts +8 -0
  266. package/src/schemas/index.ts +4 -0
  267. package/src/schemas/keywordExtraction.ts +11 -0
  268. package/src/schemas/workflowMatching.ts +29 -0
  269. package/src/services/embedded-workflow-service.ts +2224 -0
  270. package/src/services/index.ts +17 -0
  271. package/src/services/workflow-credential-store.ts +132 -0
  272. package/src/services/workflow-dispatch.ts +121 -0
  273. package/src/services/workflow-service.ts +839 -0
  274. package/src/trigger-routes.ts +714 -0
  275. package/src/types/index.ts +562 -0
  276. package/src/utils/catalog.ts +260 -0
  277. package/src/utils/clarification.ts +52 -0
  278. package/src/utils/context.ts +22 -0
  279. package/src/utils/credentialResolver.ts +234 -0
  280. package/src/utils/generation.ts +987 -0
  281. package/src/utils/host-capabilities.ts +81 -0
  282. package/src/utils/inferSyntheticOutputSchema.ts +163 -0
  283. package/src/utils/outputSchema.ts +372 -0
  284. package/src/utils/validateAndRepair.ts +610 -0
  285. package/src/utils/workflow-prompts/actionResponse.ts +16 -0
  286. package/src/utils/workflow-prompts/draftIntent.ts +22 -0
  287. package/src/utils/workflow-prompts/feasibilityCheck.ts +20 -0
  288. package/src/utils/workflow-prompts/fieldCorrection.ts +20 -0
  289. package/src/utils/workflow-prompts/index.ts +10 -0
  290. package/src/utils/workflow-prompts/keywordExtraction.ts +20 -0
  291. package/src/utils/workflow-prompts/parameterCorrection.ts +29 -0
  292. package/src/utils/workflow-prompts/workflowGeneration.ts +528 -0
  293. package/src/utils/workflow-prompts/workflowMatching.ts +22 -0
  294. package/src/utils/workflow.ts +895 -0
@@ -0,0 +1,895 @@
1
+ import { logger } from '@elizaos/core';
2
+ import type {
3
+ NodeDefinition,
4
+ NodeProperty,
5
+ OutputRefValidation,
6
+ RuntimeContext,
7
+ SchemaContent,
8
+ WorkflowDefinition,
9
+ WorkflowValidationResult,
10
+ } from '../types/index';
11
+ import { getNodeDefinition, simplifyNodeForLLM } from './catalog';
12
+ import {
13
+ fieldExistsInSchema,
14
+ getAllFieldPathsTyped,
15
+ loadOutputSchema,
16
+ loadTriggerOutputSchema,
17
+ parseExpressions,
18
+ } from './outputSchema';
19
+
20
+ function isTriggerNode(type: string): boolean {
21
+ const t = type.toLowerCase();
22
+ return t.includes('trigger') || t.includes('webhook');
23
+ }
24
+
25
+ export function validateWorkflow(workflow: WorkflowDefinition): WorkflowValidationResult {
26
+ const errors: string[] = [];
27
+ const warnings: string[] = [];
28
+
29
+ // 1. Check nodes array exists and is non-empty
30
+ if (!workflow.nodes || !Array.isArray(workflow.nodes)) {
31
+ errors.push('Missing or invalid nodes array');
32
+ return { valid: false, errors, warnings };
33
+ }
34
+
35
+ if (workflow.nodes.length === 0) {
36
+ errors.push('Workflow must have at least one node');
37
+ return { valid: false, errors, warnings };
38
+ }
39
+
40
+ // 2. Check connections structure
41
+ if (!workflow.connections || typeof workflow.connections !== 'object') {
42
+ errors.push('Missing or invalid connections object');
43
+ return { valid: false, errors, warnings };
44
+ }
45
+
46
+ // 3. Validate each node
47
+ const nodeNames = new Set<string>();
48
+ const nodeMap = new Map<string, (typeof workflow.nodes)[0]>();
49
+
50
+ for (const node of workflow.nodes) {
51
+ // Check required fields
52
+ if (!node.name || typeof node.name !== 'string') {
53
+ errors.push('Node missing name');
54
+ continue;
55
+ }
56
+
57
+ if (!node.type || typeof node.type !== 'string') {
58
+ errors.push(`Node "${node.name}" missing type`);
59
+ continue;
60
+ }
61
+
62
+ // Check for duplicate names
63
+ if (nodeNames.has(node.name)) {
64
+ errors.push(`Duplicate node name: "${node.name}"`);
65
+ }
66
+ nodeNames.add(node.name);
67
+ nodeMap.set(node.name, node);
68
+
69
+ // Check position (positionNodes() will fix this after validation)
70
+ if (!node.position || !Array.isArray(node.position) || node.position.length !== 2) {
71
+ warnings.push(`Node "${node.name}" has invalid position, will be auto-positioned`);
72
+ }
73
+
74
+ // Check parameters
75
+ if (!node.parameters || typeof node.parameters !== 'object') {
76
+ warnings.push(`Node "${node.name}" missing parameters object`);
77
+ }
78
+ }
79
+
80
+ // 4. Validate connections reference existing nodes
81
+ for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
82
+ if (!nodeNames.has(sourceName)) {
83
+ errors.push(`Connection references non-existent source node: "${sourceName}"`);
84
+ continue;
85
+ }
86
+
87
+ for (const [_outputType, connections] of Object.entries(outputs)) {
88
+ if (!Array.isArray(connections)) {
89
+ errors.push(`Invalid connection structure for node "${sourceName}"`);
90
+ continue;
91
+ }
92
+
93
+ for (const connectionGroup of connections) {
94
+ if (!Array.isArray(connectionGroup)) {
95
+ continue;
96
+ }
97
+
98
+ for (const connection of connectionGroup) {
99
+ if (!connection.node || typeof connection.node !== 'string') {
100
+ errors.push(`Invalid connection from "${sourceName}"`);
101
+ continue;
102
+ }
103
+
104
+ if (!nodeNames.has(connection.node)) {
105
+ errors.push(
106
+ `Connection references non-existent target node: "${connection.node}" (from "${sourceName}")`
107
+ );
108
+ }
109
+ }
110
+ }
111
+ }
112
+ }
113
+
114
+ // 5. Check for at least one trigger node
115
+ const hasTrigger = workflow.nodes.some(
116
+ (node) => isTriggerNode(node.type) || node.name.toLowerCase().includes('start')
117
+ );
118
+
119
+ if (!hasTrigger) {
120
+ warnings.push('Workflow has no trigger node - it can only be executed manually');
121
+ }
122
+
123
+ // 6. Check for orphan nodes (nodes with no incoming connections, except triggers)
124
+ const nodesWithIncoming = new Set<string>();
125
+ for (const outputs of Object.values(workflow.connections)) {
126
+ for (const connectionGroup of Object.values(outputs)) {
127
+ for (const connections of connectionGroup) {
128
+ for (const conn of connections) {
129
+ nodesWithIncoming.add(conn.node);
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ for (const node of workflow.nodes) {
136
+ if (
137
+ !isTriggerNode(node.type) &&
138
+ !node.name.toLowerCase().includes('start') &&
139
+ !nodesWithIncoming.has(node.name)
140
+ ) {
141
+ warnings.push(`Node "${node.name}" has no incoming connections - it will never execute`);
142
+ }
143
+ }
144
+
145
+ if (errors.length > 0) {
146
+ return { valid: false, errors, warnings };
147
+ }
148
+
149
+ return {
150
+ valid: true,
151
+ errors: [],
152
+ warnings,
153
+ };
154
+ }
155
+
156
+ export function validateNodeParameters(workflow: WorkflowDefinition): string[] {
157
+ const warnings: string[] = [];
158
+
159
+ for (const node of workflow.nodes) {
160
+ const nodeDef = getNodeDefinition(node.type);
161
+ if (!nodeDef) {
162
+ continue;
163
+ } // Unknown node type — skip
164
+
165
+ const effectiveParams = buildEffectiveParams(nodeDef, node);
166
+ for (const prop of nodeDef.properties) {
167
+ if (!prop.required) {
168
+ continue;
169
+ }
170
+ if (!isPropertyVisible(prop, effectiveParams)) {
171
+ continue;
172
+ }
173
+
174
+ const value = node.parameters?.[prop.name];
175
+ if (value === undefined || value === null || value === '') {
176
+ const label = prop.displayName || prop.name;
177
+ // Include the catalog property description in parentheses when
178
+ // present. The displayName alone is often opaque ("Name", "Type",
179
+ // "Mode") and the user has no way to know what the parameter
180
+ // actually governs. The description is the same hover-text the
181
+ // upstream node UI shows, so it carries real semantic information.
182
+ // Catalog descriptions sometimes contain raw HTML (e.g.
183
+ // <a href="...">expression</a>) sourced from the upstream
184
+ // node-types definitions; strip tags before interpolation so the
185
+ // clarification surfaces in plain-text contexts cleanly.
186
+ const description = prop.description?.replace(/<[^>]*>/g, '').trim();
187
+ const detail = description ? ` (${description})` : '';
188
+ warnings.push(`Node "${node.name}": missing required parameter "${label}"${detail}`);
189
+ }
190
+ }
191
+ }
192
+
193
+ return warnings;
194
+ }
195
+
196
+ /**
197
+ * Build effective parameters for visibility checks by applying property defaults in two passes.
198
+ *
199
+ * Pass 1: always-visible props (no displayOptions) — e.g. `resource` default "message".
200
+ * Pass 2: props whose displayOptions are satisfied by pass-1 defaults — e.g. `operation`
201
+ * default "send" becomes visible once `resource` is known.
202
+ *
203
+ * Two passes resolve the depth-2 chains present in workflows node definitions
204
+ * (root prop → one level of conditional). The `@version` key is injected as the
205
+ * node's typeVersion so displayOptions conditions that reference it work correctly.
206
+ */
207
+ function buildEffectiveParams(
208
+ nodeDef: NodeDefinition,
209
+ node: { typeVersion: number; parameters: Record<string, unknown> }
210
+ ): Record<string, unknown> {
211
+ const effective: Record<string, unknown> = { '@version': node.typeVersion };
212
+
213
+ // Pass 1: always-visible properties (no displayOptions)
214
+ for (const prop of nodeDef.properties) {
215
+ if (!prop.displayOptions && !(prop.name in node.parameters) && prop.default !== undefined) {
216
+ effective[prop.name] = prop.default;
217
+ }
218
+ }
219
+
220
+ // Merge actual params so pass 2 sees LLM-provided values (e.g. resource set
221
+ // explicitly while operation is omitted — operation's displayOptions depends on resource).
222
+ Object.assign(effective, node.parameters);
223
+
224
+ // Pass 2: properties with displayOptions that are now satisfied by pass-1 defaults + actual params
225
+ for (const prop of nodeDef.properties) {
226
+ if (
227
+ prop.displayOptions &&
228
+ !(prop.name in effective) &&
229
+ prop.default !== undefined &&
230
+ isPropertyVisible(prop, effective)
231
+ ) {
232
+ effective[prop.name] = prop.default;
233
+ }
234
+ }
235
+
236
+ return effective;
237
+ }
238
+
239
+ /**
240
+ * workflows displayOptions logic:
241
+ * - `show`: ALL conditions must match for visible
242
+ * - `hide`: ANY match hides the property
243
+ */
244
+ function isPropertyVisible(prop: NodeProperty, parameters: Record<string, unknown>): boolean {
245
+ if (!prop.displayOptions) {
246
+ return true;
247
+ }
248
+
249
+ const show = prop.displayOptions as {
250
+ show?: Record<string, unknown[]>;
251
+ hide?: Record<string, unknown[]>;
252
+ };
253
+
254
+ // If "show" is defined, ALL conditions must match
255
+ if (show.show) {
256
+ for (const [key, allowedValues] of Object.entries(show.show)) {
257
+ if (!Array.isArray(allowedValues)) {
258
+ continue;
259
+ }
260
+ const paramValue = parameters?.[key];
261
+ if (!allowedValues.includes(paramValue)) {
262
+ return false;
263
+ }
264
+ }
265
+ }
266
+
267
+ // If "hide" is defined, ANY match hides the property
268
+ if (show.hide) {
269
+ for (const [key, hiddenValues] of Object.entries(show.hide)) {
270
+ if (!Array.isArray(hiddenValues)) {
271
+ continue;
272
+ }
273
+ const paramValue = parameters?.[key];
274
+ if (hiddenValues.includes(paramValue)) {
275
+ return false;
276
+ }
277
+ }
278
+ }
279
+
280
+ return true;
281
+ }
282
+
283
+ export function validateNodeInputs(workflow: WorkflowDefinition): string[] {
284
+ const warnings: string[] = [];
285
+
286
+ // Count incoming connections per node
287
+ const incomingCount = new Map<string, number>();
288
+ for (const node of workflow.nodes) {
289
+ incomingCount.set(node.name, 0);
290
+ }
291
+ for (const outputs of Object.values(workflow.connections)) {
292
+ for (const connectionGroups of Object.values(outputs)) {
293
+ for (const connections of connectionGroups) {
294
+ for (const conn of connections) {
295
+ incomingCount.set(conn.node, (incomingCount.get(conn.node) || 0) + 1);
296
+ }
297
+ }
298
+ }
299
+ }
300
+
301
+ for (const node of workflow.nodes) {
302
+ const nodeDef = getNodeDefinition(node.type);
303
+ if (!nodeDef) {
304
+ continue;
305
+ }
306
+
307
+ if (isTriggerNode(node.type) || nodeDef.group.includes('trigger')) {
308
+ continue;
309
+ }
310
+
311
+ // Dynamic inputs (workflows expression string) can't be validated statically
312
+ if (!Array.isArray(nodeDef.inputs)) {
313
+ continue;
314
+ }
315
+
316
+ const expectedInputs = nodeDef.inputs.filter((i) => i === 'main').length;
317
+ const actualInputs = incomingCount.get(node.name) || 0;
318
+
319
+ if (expectedInputs > 0 && actualInputs < expectedInputs) {
320
+ warnings.push(
321
+ `Node "${node.name}" expects ${expectedInputs} input(s) but has ${actualInputs}`
322
+ );
323
+ }
324
+ }
325
+
326
+ return warnings;
327
+ }
328
+
329
+ export function positionNodes(workflow: WorkflowDefinition): WorkflowDefinition {
330
+ // Clone workflow
331
+ const positioned = { ...workflow };
332
+ positioned.nodes = [...workflow.nodes];
333
+
334
+ // Check if all nodes already have valid positions
335
+ const allHavePositions = positioned.nodes.every(
336
+ (node) =>
337
+ node.position &&
338
+ Array.isArray(node.position) &&
339
+ node.position.length === 2 &&
340
+ typeof node.position[0] === 'number' &&
341
+ typeof node.position[1] === 'number'
342
+ );
343
+
344
+ if (allHavePositions) {
345
+ return positioned; // No changes needed
346
+ }
347
+
348
+ // Build node graph to understand flow structure
349
+ const nodeGraph = buildNodeGraph(positioned);
350
+
351
+ // Position nodes level by level (breadth-first from triggers)
352
+ const positionedNodes = positionByLevels(positioned.nodes, nodeGraph);
353
+
354
+ positioned.nodes = positionedNodes;
355
+ return positioned;
356
+ }
357
+
358
+ /** Ensure trigger nodes use simplified output when available. */
359
+ export function normalizeTriggerSimpleParam(workflow: WorkflowDefinition): void {
360
+ for (const node of workflow.nodes) {
361
+ if (!isTriggerNode(node.type)) {
362
+ continue;
363
+ }
364
+
365
+ const def = getNodeDefinition(node.type);
366
+ const hasSimple = def?.properties?.some((p: { name: string }) => p.name === 'simple');
367
+ if (hasSimple) {
368
+ node.parameters = { ...node.parameters, simple: true };
369
+ }
370
+ }
371
+ }
372
+
373
+ /**
374
+ * Validates that $json expressions reference fields that exist in upstream node output schemas.
375
+ * Returns a list of invalid references that need correction.
376
+ */
377
+ export function validateOutputReferences(workflow: WorkflowDefinition): OutputRefValidation[] {
378
+ const invalidRefs: OutputRefValidation[] = [];
379
+ const upstreamMap = buildUpstreamMap(workflow);
380
+ const nodeMap = new Map(workflow.nodes.map((n) => [n.name, n]));
381
+
382
+ const schemaCache = new Map<
383
+ string,
384
+ {
385
+ schema: SchemaContent;
386
+ fields: string[];
387
+ node: WorkflowDefinition['nodes'][0];
388
+ } | null
389
+ >();
390
+
391
+ function getSourceSchema(sourceName: string) {
392
+ if (schemaCache.has(sourceName)) {
393
+ const cached = schemaCache.get(sourceName);
394
+ return cached === undefined ? null : cached;
395
+ }
396
+ const sourceNode = nodeMap.get(sourceName);
397
+ if (!sourceNode) {
398
+ schemaCache.set(sourceName, null);
399
+ return null;
400
+ }
401
+ const resource = (sourceNode.parameters?.resource as string) || '';
402
+ const operation = (sourceNode.parameters?.operation as string) || '';
403
+ const schemaResult = isTriggerNode(sourceNode.type)
404
+ ? loadTriggerOutputSchema(sourceNode.type, sourceNode.parameters as Record<string, unknown>)
405
+ : loadOutputSchema(sourceNode.type, resource, operation);
406
+ if (!schemaResult) {
407
+ schemaCache.set(sourceName, null);
408
+ return null;
409
+ }
410
+ const entry = {
411
+ schema: schemaResult.schema,
412
+ fields: schemaResult.fields,
413
+ node: sourceNode,
414
+ };
415
+ schemaCache.set(sourceName, entry);
416
+ return entry;
417
+ }
418
+
419
+ for (const node of workflow.nodes) {
420
+ if (!node.parameters) {
421
+ continue;
422
+ }
423
+
424
+ const expressions = parseExpressions(node.parameters);
425
+ if (expressions.length === 0) {
426
+ continue;
427
+ }
428
+
429
+ const upstreamNames = upstreamMap.get(node.name) || [];
430
+ if (upstreamNames.length === 0) {
431
+ continue;
432
+ }
433
+
434
+ const defaultSourceName = upstreamNames[0];
435
+
436
+ for (const expr of expressions) {
437
+ const sourceName = expr.sourceNodeName || defaultSourceName;
438
+ const cached = getSourceSchema(sourceName);
439
+ if (!cached) {
440
+ continue;
441
+ }
442
+
443
+ const exists = fieldExistsInSchema(expr.path, cached.schema);
444
+ if (!exists) {
445
+ const resource = (cached.node.parameters?.resource as string) || '';
446
+ const operation = (cached.node.parameters?.operation as string) || '';
447
+ invalidRefs.push({
448
+ nodeName: node.name,
449
+ expression: expr.fullExpression,
450
+ field: expr.field,
451
+ sourceNodeName: sourceName,
452
+ sourceNodeType: cached.node.type,
453
+ resource,
454
+ operation,
455
+ availableFields: getAllFieldPathsTyped(cached.schema).map((f) => `${f.path} (${f.type})`),
456
+ });
457
+ }
458
+ }
459
+ }
460
+
461
+ return invalidRefs;
462
+ }
463
+
464
+ /**
465
+ * Correct invalid option parameter values and typeVersion against catalog definitions.
466
+ * Top-level options (resource) are fixed first so displayOptions cascading works for dependent ones (operation).
467
+ */
468
+ export function correctOptionParameters(workflow: WorkflowDefinition): number {
469
+ let corrections = 0;
470
+
471
+ for (const node of workflow.nodes) {
472
+ const nodeDef = getNodeDefinition(node.type);
473
+ if (!nodeDef) {
474
+ continue;
475
+ }
476
+
477
+ if (node.type !== nodeDef.name) {
478
+ logger.warn(
479
+ { src: 'plugin:workflow:correctOptions' },
480
+ `Node "${node.name}": type "${node.type}" → "${nodeDef.name}"`
481
+ );
482
+ node.type = nodeDef.name;
483
+ corrections++;
484
+ }
485
+
486
+ const validVersions = Array.isArray(nodeDef.version) ? nodeDef.version : [nodeDef.version];
487
+ if (node.typeVersion && !validVersions.includes(node.typeVersion)) {
488
+ const maxVersion = Math.max(...validVersions);
489
+ logger.warn(
490
+ { src: 'plugin:workflow:correctOptions' },
491
+ `Node "${node.name}": typeVersion ${node.typeVersion} → ${maxVersion}`
492
+ );
493
+ node.typeVersion = maxVersion;
494
+ corrections++;
495
+ }
496
+
497
+ const topLevel: NodeProperty[] = [];
498
+ const dependent: NodeProperty[] = [];
499
+ for (const prop of nodeDef.properties) {
500
+ if (prop.type !== 'options' || !prop.options?.length) {
501
+ continue;
502
+ }
503
+ if (prop.displayOptions) {
504
+ dependent.push(prop);
505
+ } else {
506
+ topLevel.push(prop);
507
+ }
508
+ }
509
+
510
+ for (const prop of topLevel) {
511
+ corrections += fixOptionValue(node, prop);
512
+ }
513
+
514
+ const effectiveParamsForDeps = buildEffectiveParams(nodeDef, node);
515
+ for (const prop of dependent) {
516
+ if (!isPropertyVisible(prop, effectiveParamsForDeps)) {
517
+ continue;
518
+ }
519
+ corrections += fixOptionValue(node, prop);
520
+ }
521
+ }
522
+
523
+ return corrections;
524
+ }
525
+
526
+ function fixOptionValue(node: WorkflowDefinition['nodes'][0], prop: NodeProperty): number {
527
+ const currentValue = node.parameters[prop.name];
528
+ if (currentValue === undefined) {
529
+ return 0;
530
+ }
531
+
532
+ const allowedValues = prop.options?.map((o) => o.value) ?? [];
533
+ if (allowedValues.includes(currentValue as string | number | boolean)) {
534
+ return 0;
535
+ }
536
+
537
+ const corrected =
538
+ prop.default !== undefined && allowedValues.includes(prop.default as string | number | boolean)
539
+ ? prop.default
540
+ : allowedValues[0];
541
+
542
+ logger.warn(
543
+ { src: 'plugin:workflow:correctOptions' },
544
+ `Node "${node.name}": ${prop.name} "${currentValue}" → "${corrected}"`
545
+ );
546
+ node.parameters[prop.name] = corrected;
547
+ return 1;
548
+ }
549
+
550
+ function buildUpstreamMap(workflow: WorkflowDefinition): Map<string, string[]> {
551
+ const upstream = new Map<string, string[]>();
552
+
553
+ for (const node of workflow.nodes) {
554
+ upstream.set(node.name, []);
555
+ }
556
+
557
+ for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
558
+ for (const connectionGroups of Object.values(outputs)) {
559
+ for (const connections of connectionGroups) {
560
+ for (const conn of connections) {
561
+ const existing = upstream.get(conn.node) || [];
562
+ if (!existing.includes(sourceName)) {
563
+ existing.push(sourceName);
564
+ upstream.set(conn.node, existing);
565
+ }
566
+ }
567
+ }
568
+ }
569
+ }
570
+
571
+ return upstream;
572
+ }
573
+
574
+ function buildNodeGraph(workflow: WorkflowDefinition): Map<string, string[]> {
575
+ const graph = new Map<string, string[]>();
576
+
577
+ // Initialize all nodes
578
+ for (const node of workflow.nodes) {
579
+ graph.set(node.name, []);
580
+ }
581
+
582
+ // Build edges from connections
583
+ for (const [sourceName, outputs] of Object.entries(workflow.connections)) {
584
+ const targets: string[] = [];
585
+
586
+ for (const connectionGroups of Object.values(outputs)) {
587
+ for (const connections of connectionGroups) {
588
+ for (const conn of connections) {
589
+ if (conn.node) {
590
+ targets.push(conn.node);
591
+ }
592
+ }
593
+ }
594
+ }
595
+
596
+ graph.set(sourceName, targets);
597
+ }
598
+
599
+ return graph;
600
+ }
601
+
602
+ function positionByLevels(
603
+ nodes: WorkflowDefinition['nodes'],
604
+ graph: Map<string, string[]>
605
+ ): WorkflowDefinition['nodes'] {
606
+ // Find trigger/start nodes (nodes with no incoming connections)
607
+ const incomingCount = new Map<string, number>();
608
+ for (const node of nodes) {
609
+ incomingCount.set(node.name, 0);
610
+ }
611
+
612
+ for (const targets of graph.values()) {
613
+ for (const target of targets) {
614
+ incomingCount.set(target, (incomingCount.get(target) || 0) + 1);
615
+ }
616
+ }
617
+
618
+ const triggerNodes = nodes.filter((node) => incomingCount.get(node.name) === 0);
619
+
620
+ // Organize into levels
621
+ const levels: string[][] = [];
622
+ const visited = new Set<string>();
623
+ const queue: Array<{ name: string; level: number }> = [];
624
+
625
+ // Start with triggers at level 0
626
+ for (const trigger of triggerNodes) {
627
+ queue.push({ name: trigger.name, level: 0 });
628
+ }
629
+
630
+ while (queue.length > 0) {
631
+ const next = queue.shift();
632
+ if (!next) {
633
+ continue;
634
+ }
635
+ const { name, level } = next;
636
+
637
+ if (visited.has(name)) {
638
+ continue;
639
+ }
640
+ visited.add(name);
641
+
642
+ // Add to level
643
+ if (!levels[level]) {
644
+ levels[level] = [];
645
+ }
646
+ levels[level].push(name);
647
+
648
+ // Add children to next level
649
+ const children = graph.get(name) || [];
650
+ for (const child of children) {
651
+ if (!visited.has(child)) {
652
+ queue.push({ name: child, level: level + 1 });
653
+ }
654
+ }
655
+ }
656
+
657
+ // Position nodes based on levels
658
+ const positioned = [...nodes];
659
+ const nodeMap = new Map(nodes.map((node) => [node.name, node]));
660
+
661
+ const startX = 250;
662
+ const startY = 300;
663
+ const xSpacing = 250;
664
+ const ySpacing = 100;
665
+
666
+ for (let levelIndex = 0; levelIndex < levels.length; levelIndex++) {
667
+ const levelNodes = levels[levelIndex];
668
+ const x = startX + levelIndex * xSpacing;
669
+
670
+ // Center nodes vertically if multiple in same level
671
+ const totalHeight = levelNodes.length * ySpacing;
672
+ const startYForLevel = startY - totalHeight / 2;
673
+
674
+ for (let i = 0; i < levelNodes.length; i++) {
675
+ const nodeName = levelNodes[i];
676
+ const node = nodeMap.get(nodeName);
677
+
678
+ if (node) {
679
+ const y = startYForLevel + i * ySpacing;
680
+ const nodeIndex = positioned.findIndex((n) => n.name === nodeName);
681
+ if (nodeIndex !== -1) {
682
+ positioned[nodeIndex] = {
683
+ ...positioned[nodeIndex],
684
+ position: [x, y],
685
+ };
686
+ }
687
+ }
688
+ }
689
+ }
690
+
691
+ return positioned;
692
+ }
693
+
694
+ /**
695
+ * Detect parameters not matching any VISIBLE catalog property.
696
+ * e.g. `model` is only valid for `resource: "image"`, not `resource: "text"` (where `modelId` is correct).
697
+ * Runs AFTER correctOptionParameters so resource/operation are already valid.
698
+ */
699
+ export interface UnknownParamDetection {
700
+ nodeName: string;
701
+ nodeType: string;
702
+ currentParams: Record<string, unknown>;
703
+ unknownKeys: string[];
704
+ /** Simplified property definitions for this node (used by the LLM to fix params). */
705
+ propertyDefs: NodeProperty[];
706
+ }
707
+
708
+ export function detectUnknownParameters(workflow: WorkflowDefinition): UnknownParamDetection[] {
709
+ const detections: UnknownParamDetection[] = [];
710
+
711
+ for (const node of workflow.nodes) {
712
+ const nodeDef = getNodeDefinition(node.type);
713
+ if (!nodeDef || !node.parameters) {
714
+ continue;
715
+ }
716
+
717
+ // Compute visible property names using effective parameters (actual + defaults).
718
+ // Defaults are applied for always-visible props first, then for newly-visible props,
719
+ // so that chained displayOptions (resource → operation → field) resolve correctly.
720
+ const effectiveParams = buildEffectiveParams(nodeDef, node);
721
+ const visibleNames = new Set<string>();
722
+ for (const prop of nodeDef.properties) {
723
+ if (isPropertyVisible(prop, effectiveParams)) {
724
+ visibleNames.add(prop.name);
725
+ }
726
+ }
727
+
728
+ const unknownKeys: string[] = [];
729
+ for (const key of Object.keys(node.parameters)) {
730
+ if (!visibleNames.has(key)) {
731
+ unknownKeys.push(key);
732
+ }
733
+ }
734
+
735
+ if (unknownKeys.length === 0) {
736
+ continue;
737
+ }
738
+
739
+ // Provide simplified visible properties for the LLM correction prompt
740
+ const simplified = simplifyNodeForLLM(nodeDef);
741
+ const visibleSimplified = simplified.properties.filter((p) => visibleNames.has(p.name));
742
+
743
+ detections.push({
744
+ nodeName: node.name,
745
+ nodeType: node.type,
746
+ currentParams: node.parameters,
747
+ unknownKeys,
748
+ propertyDefs: visibleSimplified,
749
+ });
750
+ }
751
+
752
+ return detections;
753
+ }
754
+
755
+ /**
756
+ * Prefix all string parameter values containing {{ }} with = so workflows evaluates them as expressions.
757
+ * Without =, workflows treats {{ }} as literal text.
758
+ * Returns the number of values prefixed.
759
+ */
760
+ /**
761
+ * Deterministically attach a `credentials` block to every node that requires
762
+ * one. Runs after LLM generation as a safety net: even with a hardened
763
+ * `MANDATORY INVARIANT` rule in the system prompt, the LLM occasionally omits
764
+ * the block — and resolveCredentials only fires when a block is present, so
765
+ * an omission means the credential never gets minted server-side and the user
766
+ * has to wire it in workflows's UI.
767
+ *
768
+ * Selection rule:
769
+ * 1. Skip nodes that already have at least one credentials entry.
770
+ * 2. Look up the node's catalog definition. If `def.credentials` is empty,
771
+ * the node doesn't need credentials — skip.
772
+ * 3. Pick the first credential type from `def.credentials` that:
773
+ * - is listed in `runtimeContext.supportedCredentials.nodeTypes` for
774
+ * this node's type, AND
775
+ * - matches the node's `parameters.authentication` (when the credential's
776
+ * displayOptions.show.authentication is set; otherwise unconditional).
777
+ * 4. Inject `node.credentials = { [credType]: { id: "{{CREDENTIAL_ID}}", name } }`.
778
+ * The plugin's `resolveCredentials` later replaces `{{CREDENTIAL_ID}}` with
779
+ * the real workflows credential id.
780
+ *
781
+ * Returns the number of nodes that received an injected block (for logging).
782
+ */
783
+ export function injectMissingCredentialBlocks(
784
+ workflow: WorkflowDefinition,
785
+ relevantNodes: NodeDefinition[],
786
+ runtimeContext: RuntimeContext | undefined
787
+ ): number {
788
+ if (!runtimeContext?.supportedCredentials?.length) {
789
+ return 0;
790
+ }
791
+ // Build supportedCredType-by-nodeType lookup. Each supportedCredential entry
792
+ // applies to one or more node types; flip that map so we can ask
793
+ // "for this node type, which cred types does the host support?".
794
+ const supportedByNodeType = new Map<string, Map<string, string>>();
795
+ for (const sc of runtimeContext.supportedCredentials) {
796
+ for (const nodeType of sc.nodeTypes) {
797
+ if (!supportedByNodeType.has(nodeType)) {
798
+ supportedByNodeType.set(nodeType, new Map());
799
+ }
800
+ supportedByNodeType.get(nodeType)?.set(sc.credType, sc.friendlyName);
801
+ }
802
+ }
803
+ if (supportedByNodeType.size === 0) {
804
+ return 0;
805
+ }
806
+ const defByType = new Map(relevantNodes.map((n) => [n.name, n]));
807
+ let injected = 0;
808
+ for (const node of workflow.nodes) {
809
+ if (node.credentials && Object.keys(node.credentials).length > 0) {
810
+ continue;
811
+ }
812
+ const def = defByType.get(node.type);
813
+ if (!def?.credentials?.length) {
814
+ continue;
815
+ }
816
+ const supportedForType = supportedByNodeType.get(node.type);
817
+ if (!supportedForType?.size) {
818
+ continue;
819
+ }
820
+ // Resolve which credential type matches this node's authentication choice.
821
+ // workflows nodes typically gate credentials by `displayOptions.show.authentication`
822
+ // (e.g. discord's discordBotApi shows when authentication=botToken).
823
+ const auth =
824
+ typeof node.parameters?.authentication === 'string'
825
+ ? (node.parameters.authentication as string)
826
+ : null;
827
+ const candidate = def.credentials.find((c) => {
828
+ if (!supportedForType.has(c.name)) {
829
+ return false;
830
+ }
831
+ const showOpts = (c.displayOptions as { show?: { authentication?: string[] } } | undefined)
832
+ ?.show;
833
+ if (showOpts?.authentication && showOpts.authentication.length > 0) {
834
+ return auth ? showOpts.authentication.includes(auth) : false;
835
+ }
836
+ // Unconditional credential or no show-rule: take it.
837
+ return true;
838
+ });
839
+ if (!candidate) {
840
+ continue;
841
+ }
842
+ const friendlyName = supportedForType.get(candidate.name) ?? candidate.name;
843
+ node.credentials = {
844
+ [candidate.name]: {
845
+ id: '{{CREDENTIAL_ID}}',
846
+ name: friendlyName,
847
+ },
848
+ };
849
+ logger.debug(
850
+ {
851
+ src: 'plugin:workflow:utils:workflow',
852
+ node: node.name,
853
+ nodeType: node.type,
854
+ credType: candidate.name,
855
+ },
856
+ 'Injected missing credentials block on node (LLM omitted it)'
857
+ );
858
+ injected++;
859
+ }
860
+ return injected;
861
+ }
862
+
863
+ export function ensureExpressionPrefix(workflow: WorkflowDefinition): number {
864
+ let count = 0;
865
+ for (const node of workflow.nodes) {
866
+ if (!node.parameters) {
867
+ continue;
868
+ }
869
+ count += prefixExpressions(node.parameters);
870
+ }
871
+ return count;
872
+ }
873
+
874
+ function prefixExpressions(obj: Record<string, unknown>): number {
875
+ let count = 0;
876
+ for (const key of Object.keys(obj)) {
877
+ const value = obj[key];
878
+ if (typeof value === 'string' && value.includes('{{') && !value.startsWith('=')) {
879
+ obj[key] = `=${value}`;
880
+ count++;
881
+ } else if (Array.isArray(value)) {
882
+ for (let i = 0; i < value.length; i++) {
883
+ if (typeof value[i] === 'string' && value[i].includes('{{') && !value[i].startsWith('=')) {
884
+ value[i] = `=${value[i]}`;
885
+ count++;
886
+ } else if (typeof value[i] === 'object' && value[i] !== null) {
887
+ count += prefixExpressions(value[i] as Record<string, unknown>);
888
+ }
889
+ }
890
+ } else if (typeof value === 'object' && value !== null) {
891
+ count += prefixExpressions(value as Record<string, unknown>);
892
+ }
893
+ }
894
+ return count;
895
+ }