@solidxai/core 0.1.13-beta.3 → 0.1.13-beta.5

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 (141) hide show
  1. package/dist/commands/seed.command.d.ts +2 -0
  2. package/dist/commands/seed.command.d.ts.map +1 -1
  3. package/dist/commands/seed.command.js +21 -0
  4. package/dist/commands/seed.command.js.map +1 -1
  5. package/dist/controllers/chatter-message.controller.d.ts +5 -0
  6. package/dist/controllers/chatter-message.controller.d.ts.map +1 -1
  7. package/dist/controllers/chatter-message.controller.js +14 -0
  8. package/dist/controllers/chatter-message.controller.js.map +1 -1
  9. package/dist/entities/workflow-trigger-execution.entity.js +1 -1
  10. package/dist/entities/workflow-trigger-execution.entity.js.map +1 -1
  11. package/dist/index.d.ts +2 -0
  12. package/dist/index.d.ts.map +1 -1
  13. package/dist/index.js +2 -0
  14. package/dist/index.js.map +1 -1
  15. package/dist/interfaces.d.ts +1 -0
  16. package/dist/interfaces.d.ts.map +1 -1
  17. package/dist/interfaces.js.map +1 -1
  18. package/dist/seeders/module-metadata-seeder.service.d.ts.map +1 -1
  19. package/dist/seeders/module-metadata-seeder.service.js +44 -27
  20. package/dist/seeders/module-metadata-seeder.service.js.map +1 -1
  21. package/dist/seeders/seed-data/solid-core-metadata.json +84 -70
  22. package/dist/services/chatter-message.service.d.ts +5 -0
  23. package/dist/services/chatter-message.service.d.ts.map +1 -1
  24. package/dist/services/chatter-message.service.js +18 -0
  25. package/dist/services/chatter-message.service.js.map +1 -1
  26. package/dist/services/workflow/nodes/execution-fail.node.d.ts +6 -0
  27. package/dist/services/workflow/nodes/execution-fail.node.d.ts.map +1 -0
  28. package/dist/services/workflow/nodes/execution-fail.node.js +81 -0
  29. package/dist/services/workflow/nodes/execution-fail.node.js.map +1 -0
  30. package/dist/services/workflow/nodes/for-each.node.d.ts +2 -0
  31. package/dist/services/workflow/nodes/for-each.node.d.ts.map +1 -1
  32. package/dist/services/workflow/nodes/for-each.node.js +114 -5
  33. package/dist/services/workflow/nodes/for-each.node.js.map +1 -1
  34. package/dist/services/workflow/nodes/if.node.d.ts.map +1 -1
  35. package/dist/services/workflow/nodes/if.node.js +7 -0
  36. package/dist/services/workflow/nodes/if.node.js.map +1 -1
  37. package/dist/services/workflow/nodes/log-write.node.d.ts.map +1 -1
  38. package/dist/services/workflow/nodes/log-write.node.js +2 -0
  39. package/dist/services/workflow/nodes/log-write.node.js.map +1 -1
  40. package/dist/services/workflow/nodes/loop-until.node.d.ts +12 -0
  41. package/dist/services/workflow/nodes/loop-until.node.d.ts.map +1 -0
  42. package/dist/services/workflow/nodes/loop-until.node.js +281 -0
  43. package/dist/services/workflow/nodes/loop-until.node.js.map +1 -0
  44. package/dist/services/workflow/nodes/parallel.node.d.ts.map +1 -1
  45. package/dist/services/workflow/nodes/parallel.node.js +7 -0
  46. package/dist/services/workflow/nodes/parallel.node.js.map +1 -1
  47. package/dist/services/workflow/nodes/sequential.node.d.ts.map +1 -1
  48. package/dist/services/workflow/nodes/sequential.node.js +7 -0
  49. package/dist/services/workflow/nodes/sequential.node.js.map +1 -1
  50. package/dist/services/workflow/nodes/switch.node.d.ts.map +1 -1
  51. package/dist/services/workflow/nodes/switch.node.js +7 -0
  52. package/dist/services/workflow/nodes/switch.node.js.map +1 -1
  53. package/dist/services/workflow/workflow-definition-validator.service.d.ts.map +1 -1
  54. package/dist/services/workflow/workflow-definition-validator.service.js +5 -2
  55. package/dist/services/workflow/workflow-definition-validator.service.js.map +1 -1
  56. package/dist/services/workflow/workflow-expression.service.d.ts.map +1 -1
  57. package/dist/services/workflow/workflow-expression.service.js +5 -0
  58. package/dist/services/workflow/workflow-expression.service.js.map +1 -1
  59. package/dist/services/workflow/workflow-node-registry.service.d.ts +2 -1
  60. package/dist/services/workflow/workflow-node-registry.service.d.ts.map +1 -1
  61. package/dist/services/workflow/workflow-node-registry.service.js +7 -0
  62. package/dist/services/workflow/workflow-node-registry.service.js.map +1 -1
  63. package/dist/services/workflow/workflow-runtime.service.d.ts +8 -0
  64. package/dist/services/workflow/workflow-runtime.service.d.ts.map +1 -1
  65. package/dist/services/workflow/workflow-runtime.service.js +197 -32
  66. package/dist/services/workflow/workflow-runtime.service.js.map +1 -1
  67. package/dist/solid-core.module.d.ts.map +1 -1
  68. package/dist/solid-core.module.js +4 -0
  69. package/dist/solid-core.module.js.map +1 -1
  70. package/dist/types/workflow-dsl.types.d.ts +14 -0
  71. package/dist/types/workflow-dsl.types.d.ts.map +1 -1
  72. package/dist/types/workflow-dsl.types.js.map +1 -1
  73. package/dist-tests/api/authenticate.spec.js +119 -0
  74. package/dist-tests/api/authenticate.spec.js.map +1 -0
  75. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js +97 -0
  76. package/dist-tests/api/crud-service.findOne.cityMaster.spec.js.map +1 -0
  77. package/dist-tests/api/ping.spec.js +21 -0
  78. package/dist-tests/api/ping.spec.js.map +1 -0
  79. package/dist-tests/helpers/auth.js +41 -0
  80. package/dist-tests/helpers/auth.js.map +1 -0
  81. package/dist-tests/helpers/env.js +11 -0
  82. package/dist-tests/helpers/env.js.map +1 -0
  83. package/docs/agent-builder/notebook-lm-prompt-for-agent-builder.md +136 -0
  84. package/docs/agent-builder/team-ready-note-agent-builder.md +305 -0
  85. package/docs/agent-hub-grooming.md +301 -0
  86. package/docs/dashboards/AGENTIC_DASHBOARD_IMPLEMENTATION_PLAN.md +438 -0
  87. package/docs/dashboards/dashboard-curl-smoke-tests.txt +146 -0
  88. package/docs/dashboards/delete-legacy-dashboard-metadata.sql +172 -0
  89. package/docs/datasource-introspection-ddl-analysis.md +326 -0
  90. package/docs/datasource-introspection-implementation-plan.md +306 -0
  91. package/docs/grouping-enhancements.md +89 -0
  92. package/docs/java-spring/README.md +3 -0
  93. package/docs/java-spring/solid-core-module-deep-dive-report.md +1317 -0
  94. package/docs/module-package-import-handoff.md +691 -0
  95. package/docs/seed-changes.md +65 -0
  96. package/docs/test-data-workflow.md +200 -0
  97. package/docs/type-declaration-import-issue.md +24 -0
  98. package/docs/workflow/business-automation-example-notes.md +309 -0
  99. package/docs/workflow/control-flow-node-addition-sop.md +324 -0
  100. package/docs/workflow/data-engineering-pipeline-example-notes.md +330 -0
  101. package/docs/workflow/foreach-example-notes.md +187 -0
  102. package/docs/workflow/hello-world-example-notes.md +271 -0
  103. package/docs/workflow/kestra-concepts-plugins-blueprints.md +315 -0
  104. package/docs/workflow/loop-until-example-notes.md +198 -0
  105. package/docs/workflow/microservices-and-apis-example-notes.md +264 -0
  106. package/docs/workflow/samples.md +25 -0
  107. package/docs/workflow/what-is-kestra.md +79 -0
  108. package/docs/workflow/workflow-core-module-handoff-summary.md +191 -0
  109. package/docs/workflow/workflow-module-brd.md +185 -0
  110. package/docs/workflow/workflow-module-domain-model.md +252 -0
  111. package/docs/workflow/workflow-module-metadata-dsl.md +302 -0
  112. package/docs/workflow/workflow-module-technical-spec.md +293 -0
  113. package/docs/workflow/workflow-node-type-contracts.md +635 -0
  114. package/docs/workflow/workflow-node-ui-metadata-contract.md +1262 -0
  115. package/docs/workflow/workflow-solidx-model-field-spec.md +284 -0
  116. package/package.json +1 -1
  117. package/src/commands/seed.command.ts +19 -0
  118. package/src/controllers/chatter-message.controller.ts +8 -0
  119. package/src/entities/workflow-trigger-execution.entity.ts +1 -1
  120. package/src/index.ts +2 -0
  121. package/src/interfaces.ts +1 -0
  122. package/src/seeders/module-metadata-seeder.service.ts +50 -36
  123. package/src/seeders/seed-data/solid-core-metadata.json +84 -70
  124. package/src/services/chatter-message.service.ts +26 -0
  125. package/src/services/workflow/nodes/execution-fail.node.ts +82 -0
  126. package/src/services/workflow/nodes/for-each.node.ts +140 -6
  127. package/src/services/workflow/nodes/if.node.ts +7 -0
  128. package/src/services/workflow/nodes/log-write.node.ts +2 -0
  129. package/src/services/workflow/nodes/loop-until.node.ts +343 -0
  130. package/src/services/workflow/nodes/parallel.node.ts +7 -0
  131. package/src/services/workflow/nodes/sequential.node.ts +7 -0
  132. package/src/services/workflow/nodes/switch.node.ts +7 -0
  133. package/src/services/workflow/workflow-definition-validator.service.ts +5 -2
  134. package/src/services/workflow/workflow-expression.service.ts +5 -0
  135. package/src/services/workflow/workflow-node-registry.service.ts +11 -0
  136. package/src/services/workflow/workflow-runtime.service.ts +274 -39
  137. package/src/solid-core.module.ts +4 -0
  138. package/src/types/workflow-dsl.types.ts +43 -0
  139. package/.claude/settings.local.json +0 -16
  140. package/CLAUDE.md +0 -26
  141. package/src/services/1.js +0 -6
@@ -6141,7 +6141,7 @@
6141
6141
  "type": "int",
6142
6142
  "ormType": "integer",
6143
6143
  "isSystem": false,
6144
- "defaultValue": 1,
6144
+ "defaultValue": "1",
6145
6145
  "min": null,
6146
6146
  "max": null,
6147
6147
  "required": true,
@@ -6162,7 +6162,7 @@
6162
6162
  "type": "int",
6163
6163
  "ormType": "integer",
6164
6164
  "isSystem": false,
6165
- "defaultValue": 0,
6165
+ "defaultValue": "0",
6166
6166
  "min": null,
6167
6167
  "max": null,
6168
6168
  "required": false,
@@ -7367,7 +7367,7 @@
7367
7367
  "type": "boolean",
7368
7368
  "ormType": "boolean",
7369
7369
  "isSystem": false,
7370
- "defaultValue": false,
7370
+ "defaultValue": "false",
7371
7371
  "required": false,
7372
7372
  "unique": false,
7373
7373
  "index": true,
@@ -7868,6 +7868,7 @@
7868
7868
  "ChatterMessageController.findMany",
7869
7869
  "ChatterMessageController.markCompleted",
7870
7870
  "ChatterMessageController.updateCustomNoteMessage",
7871
+ "ChatterMessageController.getMentionableUsers",
7871
7872
  "ListOfValuesController.findMany",
7872
7873
  "ListOfValuesController.findOne",
7873
7874
  "SettingController.getAllSettings",
@@ -8681,16 +8682,42 @@
8681
8682
  {
8682
8683
  "displayName": "Workflow Definitions",
8683
8684
  "name": "workflowDefinition-list-action",
8684
- "type": "custom",
8685
+ "type": "solid",
8685
8686
  "domain": "",
8686
8687
  "context": "",
8687
- "customComponent": "/admin/core/solid-core/workflow-definition/list",
8688
+ "customComponent": "",
8688
8689
  "customIsModal": true,
8689
8690
  "serverEndpoint": "",
8690
8691
  "viewUserKey": "workflowDefinition-list-view",
8691
8692
  "moduleUserKey": "solid-core",
8692
8693
  "modelUserKey": "workflowDefinition"
8693
8694
  },
8695
+ {
8696
+ "displayName": "New Workflow Definition",
8697
+ "name": "workflowDefinition-create-action",
8698
+ "type": "custom",
8699
+ "domain": "",
8700
+ "context": "{}",
8701
+ "customComponent": "/admin/core/solid-core/workflow-definition/editor/new",
8702
+ "customIsModal": false,
8703
+ "serverEndpoint": "",
8704
+ "viewUserKey": "workflowDefinition-list-view",
8705
+ "moduleUserKey": "solid-core",
8706
+ "modelUserKey": "workflowDefinition"
8707
+ },
8708
+ {
8709
+ "displayName": "Workflow Definition Editor",
8710
+ "name": "workflowDefinition-editor-action",
8711
+ "type": "custom",
8712
+ "domain": "",
8713
+ "context": "{}",
8714
+ "customComponent": "/admin/core/solid-core/workflow-definition/editor",
8715
+ "customIsModal": false,
8716
+ "serverEndpoint": "",
8717
+ "viewUserKey": "workflowDefinition-list-view",
8718
+ "moduleUserKey": "solid-core",
8719
+ "modelUserKey": "workflowDefinition"
8720
+ },
8694
8721
  {
8695
8722
  "displayName": "Workflow Execution List Action",
8696
8723
  "name": "workflowExecution-list-action",
@@ -17260,97 +17287,71 @@
17260
17287
  ],
17261
17288
  "enableGlobalSearch": true,
17262
17289
  "create": true,
17290
+ "createAction": "workflowDefinition-create-action",
17263
17291
  "edit": true,
17292
+ "editAction": "workflowDefinition-editor-action",
17264
17293
  "delete": true,
17265
- "headerButtons": [
17266
- {
17267
- "attrs": {
17268
- "label": "Open Workflow Editor",
17269
- "action": "openWorkflowDefinitionEditorHeaderAction",
17270
- "actionInContextMenu": false,
17271
- "openInPopup": false,
17272
- "icon": "si-sparkles"
17273
- }
17274
- }
17275
- ],
17276
- "rowButtons": [
17277
- {
17278
- "attrs": {
17279
- "className": "p-button-text",
17280
- "icon": "si-pencil",
17281
- "label": "Open Editor",
17282
- "action": "openWorkflowDefinitionEditorRowAction",
17283
- "openInPopup": false,
17284
- "actionInContextMenu": true
17285
- }
17286
- }
17287
- ]
17294
+ "showRowActionsAsIconOnly": true,
17295
+ "showRowActionsInContextMenu": false
17288
17296
  },
17289
17297
  "children": [
17290
17298
  {
17291
17299
  "type": "field",
17292
17300
  "attrs": {
17293
- "name": "id"
17294
- }
17295
- },
17296
- {
17297
- "type": "field",
17298
- "attrs": {
17299
- "name": "key"
17300
- }
17301
- },
17302
- {
17303
- "type": "field",
17304
- "attrs": {
17305
- "name": "moduleMetadata"
17306
- }
17307
- },
17308
- {
17309
- "type": "field",
17310
- "attrs": {
17311
- "name": "displayName"
17312
- }
17313
- },
17314
- {
17315
- "type": "field",
17316
- "attrs": {
17317
- "name": "namespace"
17318
- }
17319
- },
17320
- {
17321
- "type": "field",
17322
- "attrs": {
17323
- "name": "description"
17301
+ "name": "key",
17302
+ "className": "font-bold",
17303
+ "sortable": true,
17304
+ "isSearchable": true,
17305
+ "truncateAfter": 28
17324
17306
  }
17325
17307
  },
17326
17308
  {
17327
17309
  "type": "field",
17328
17310
  "attrs": {
17329
- "name": "status"
17311
+ "name": "displayName",
17312
+ "className": "font-bold",
17313
+ "sortable": true,
17314
+ "isSearchable": true,
17315
+ "truncateAfter": 32
17330
17316
  }
17331
17317
  },
17332
17318
  {
17333
17319
  "type": "field",
17334
17320
  "attrs": {
17335
- "name": "definitionVersion"
17321
+ "name": "namespace",
17322
+ "sortable": true,
17323
+ "isSearchable": true,
17324
+ "truncateAfter": 28
17336
17325
  }
17337
17326
  },
17338
17327
  {
17339
17328
  "type": "field",
17340
17329
  "attrs": {
17341
- "name": "definitionChecksum"
17330
+ "name": "description",
17331
+ "isSearchable": true,
17332
+ "truncateAfter": 56
17342
17333
  }
17343
17334
  },
17344
17335
  {
17345
17336
  "type": "field",
17346
17337
  "attrs": {
17347
- "name": "definitionYaml"
17338
+ "name": "status",
17339
+ "sortable": true,
17340
+ "viewWidget": "PillWidget",
17341
+ "pillColorMap": {
17342
+ "draft": "#e5e7eb",
17343
+ "active": "#dcfce7",
17344
+ "inactive": "#fef3c7",
17345
+ "archived": "#ede9fe",
17346
+ "default": "#e5e7eb"
17347
+ }
17348
17348
  }
17349
17349
  },
17350
17350
  {
17351
17351
  "type": "field",
17352
17352
  "attrs": {
17353
- "name": "tags"
17353
+ "name": "definitionVersion",
17354
+ "sortable": true
17354
17355
  }
17355
17356
  }
17356
17357
  ]
@@ -17518,7 +17519,7 @@
17518
17519
  50
17519
17520
  ],
17520
17521
  "enableGlobalSearch": true,
17521
- "create": true,
17522
+ "create": false,
17522
17523
  "edit": true,
17523
17524
  "editAction": "workflowExecution-detail-action",
17524
17525
  "delete": true,
@@ -17539,8 +17540,10 @@
17539
17540
  "type": "field",
17540
17541
  "attrs": {
17541
17542
  "name": "executionIdentifier",
17543
+ "className": "font-bold",
17542
17544
  "sortable": true,
17543
- "isSearchable": true
17545
+ "isSearchable": true,
17546
+ "truncateAfter": 28
17544
17547
  }
17545
17548
  },
17546
17549
  {
@@ -17549,7 +17552,8 @@
17549
17552
  "name": "workflowDefinition",
17550
17553
  "sortable": true,
17551
17554
  "isSearchable": true,
17552
- "searchField": "workflowDefinition.displayName"
17555
+ "searchField": "workflowDefinition.displayName",
17556
+ "truncateAfter": 30
17553
17557
  }
17554
17558
  },
17555
17559
  {
@@ -17558,7 +17562,8 @@
17558
17562
  "name": "workflowDisplayName",
17559
17563
  "className": "font-bold",
17560
17564
  "sortable": true,
17561
- "isSearchable": true
17565
+ "isSearchable": true,
17566
+ "truncateAfter": 32
17562
17567
  }
17563
17568
  },
17564
17569
  {
@@ -17583,7 +17588,15 @@
17583
17588
  "attrs": {
17584
17589
  "name": "triggerType",
17585
17590
  "sortable": true,
17586
- "isSearchable": true
17591
+ "isSearchable": true,
17592
+ "viewWidget": "PillWidget",
17593
+ "pillColorMap": {
17594
+ "manual": "#ede9fe",
17595
+ "schedule": "#e0f2fe",
17596
+ "webhook": "#ccfbf1",
17597
+ "api": "#dbeafe",
17598
+ "default": "#e5e7eb"
17599
+ }
17587
17600
  }
17588
17601
  },
17589
17602
  {
@@ -17611,7 +17624,8 @@
17611
17624
  "type": "field",
17612
17625
  "attrs": {
17613
17626
  "name": "errorSummary",
17614
- "isSearchable": true
17627
+ "isSearchable": true,
17628
+ "truncateAfter": 56
17615
17629
  }
17616
17630
  }
17617
17631
  ]
@@ -21,6 +21,7 @@ import { CRUDService } from 'src/services/crud.service';
21
21
  import { MediaStorageProviderType } from '../dtos/create-media-storage-provider-metadata.dto';
22
22
  import { ChatterMessageDetails } from '../entities/chatter-message-details.entity';
23
23
  import { ChatterMessage } from '../entities/chatter-message.entity';
24
+ import { User } from '../entities/user.entity';
24
25
  import { getMediaStorageProvider } from './mediaStorageProviders';
25
26
  import { RequestContextService } from './request-context.service';
26
27
  import { Logger } from '@nestjs/common';
@@ -188,6 +189,31 @@ export class ChatterMessageService extends CRUDService<ChatterMessage> {
188
189
  }
189
190
  }
190
191
 
192
+ // Deliberately bypasses UserRepository's security-rule filtering: the mention picker
193
+ // needs id/username/fullName for any active user regardless of the caller's row-level
194
+ // access to the User model, same as the getChatterMessages one-to-many lookup below.
195
+ async getMentionableUsers(search?: string, limit: number = 8): Promise<Array<{ id: number; username: string; fullName: string }>> {
196
+ const normalizedLimit = Number.isInteger(limit) && limit > 0 && limit <= 50 ? limit : 8;
197
+ const userRepository = this.entityManager.getRepository(User);
198
+
199
+ const qb = userRepository
200
+ .createQueryBuilder('user')
201
+ .select(['user.id', 'user.username', 'user.fullName'])
202
+ .where('user.active = :active', { active: true });
203
+
204
+ const trimmedSearch = (search ?? '').trim();
205
+ if (trimmedSearch) {
206
+ qb.andWhere('(LOWER(user.username) LIKE :search OR LOWER(user.fullName) LIKE :search)', {
207
+ search: `%${trimmedSearch.toLowerCase()}%`,
208
+ });
209
+ }
210
+
211
+ qb.orderBy('user.username', 'ASC').take(normalizedLimit);
212
+
213
+ const users = await qb.getMany();
214
+ return users.map(user => ({ id: user.id, username: user.username, fullName: user.fullName }));
215
+ }
216
+
191
217
  async markCompleted(id: number) {
192
218
  const activeUser = this.requestContextService.getActiveUser();
193
219
  if (!activeUser) {
@@ -0,0 +1,82 @@
1
+ import { BadRequestException } from '@nestjs/common';
2
+ import { WorkflowNodeProvider } from '../../../decorators/workflow-node-provider.decorator';
3
+ import { WorkflowNodeHandler } from '../../../interfaces/workflow-node-handler.interface';
4
+ import {
5
+ WorkflowNodeExecutionContext,
6
+ WorkflowNodeHandlerResult,
7
+ } from '../../../types/workflow-dsl.types';
8
+
9
+ @WorkflowNodeProvider({
10
+ type: 'execution.fail',
11
+ kind: 'task',
12
+ version: '1.0.0',
13
+ category: 'core',
14
+ subcategory: 'execution',
15
+ label: 'Fail Execution',
16
+ description: 'Fails intentionally with a configured message.',
17
+ tags: ['failure', 'error', 'testing'],
18
+ aliases: ['fail'],
19
+ configSchema: {
20
+ type: 'object',
21
+ properties: {
22
+ message: {
23
+ type: 'string',
24
+ default: 'Execution failed intentionally.',
25
+ },
26
+ },
27
+ },
28
+ authoring: {
29
+ defaultConfiguration: {
30
+ message: 'Execution failed intentionally.',
31
+ },
32
+ configurationFields: [
33
+ {
34
+ key: 'message',
35
+ label: 'Message',
36
+ description: 'Failure message. Expressions are allowed.',
37
+ valueType: 'string',
38
+ expressionAllowed: true,
39
+ group: 'General',
40
+ },
41
+ ],
42
+ supportsExpressions: true,
43
+ supportsDisableToggle: true,
44
+ supportsName: true,
45
+ supportsDescription: true,
46
+ searchableText: ['fail', 'failure', 'error', 'testing'],
47
+ },
48
+ runtime: {
49
+ emitsLogs: false,
50
+ emitsArtifacts: false,
51
+ deterministicOutputs: true,
52
+ executionMode: 'task',
53
+ successStatuses: [],
54
+ },
55
+ documentation: {
56
+ summary:
57
+ 'Use this node in reference workflows and tests to intentionally exercise failure paths and error handlers.',
58
+ },
59
+ ui: {
60
+ icon: 'si-x-circle',
61
+ iconColor: '#dc2626',
62
+ iconBackgroundColor: '#fee2e2',
63
+ iconBorderColor: '#fecaca',
64
+ modalSize: 'lg',
65
+ layoutHints: {
66
+ groupOrder: ['General'],
67
+ },
68
+ },
69
+ })
70
+ export class ExecutionFailNode implements WorkflowNodeHandler {
71
+ async execute(
72
+ context: WorkflowNodeExecutionContext,
73
+ ): Promise<WorkflowNodeHandlerResult> {
74
+ const configuration = context.expression.interpolate(
75
+ context.node.configuration ?? {},
76
+ context,
77
+ );
78
+ throw new BadRequestException(
79
+ configuration.message ?? 'Execution failed intentionally.',
80
+ );
81
+ }
82
+ }
@@ -16,6 +16,8 @@ import {
16
16
  authoring: {
17
17
  defaultConfiguration: {
18
18
  items: '',
19
+ concurrencyLimit: 1,
20
+ aggregateOutputs: false,
19
21
  },
20
22
  configurationFields: [
21
23
  {
@@ -32,6 +34,35 @@ import {
32
34
  },
33
35
  },
34
36
  },
37
+ {
38
+ key: 'concurrencyLimit',
39
+ label: 'Concurrency Limit',
40
+ description:
41
+ 'Maximum number of loop iterations to run at once. Use 1 for sequential execution and 0 for unlimited concurrency.',
42
+ valueType: 'number',
43
+ defaultValue: 1,
44
+ expressionAllowed: true,
45
+ group: 'General',
46
+ },
47
+ {
48
+ key: 'aggregateOutputs',
49
+ label: 'Aggregate Outputs',
50
+ description:
51
+ 'Adds per-iteration child outputs to this forEach node output for downstream expressions.',
52
+ valueType: 'boolean',
53
+ defaultValue: false,
54
+ expressionAllowed: true,
55
+ group: 'General',
56
+ },
57
+ {
58
+ key: 'outputKey',
59
+ label: 'Output Key',
60
+ description:
61
+ 'Optional expression used as the per-iteration key when aggregate outputs is enabled.',
62
+ valueType: 'string',
63
+ expressionAllowed: true,
64
+ group: 'General',
65
+ },
35
66
  ],
36
67
  childSlots: [
37
68
  {
@@ -42,6 +73,13 @@ import {
42
73
  layout: 'sequential',
43
74
  required: true,
44
75
  },
76
+ {
77
+ key: 'errors',
78
+ label: 'Errors',
79
+ description: 'Nodes that run sequentially if this loop fails.',
80
+ kind: 'sequence',
81
+ layout: 'sequential',
82
+ },
45
83
  ],
46
84
  supportsDescription: true,
47
85
  supportsName: true,
@@ -66,32 +104,128 @@ export class ForEachNode implements WorkflowNodeHandler {
66
104
  ): Promise<WorkflowNodeHandlerResult> {
67
105
  const configuration = context.node.configuration ?? {};
68
106
  const items = context.expression.interpolate(configuration.items ?? [], context);
107
+ const concurrencyLimit = this.resolveConcurrencyLimit(
108
+ context.expression.interpolate(configuration.concurrencyLimit ?? 1, context),
109
+ );
110
+ const aggregateOutputs = Boolean(
111
+ context.expression.interpolate(configuration.aggregateOutputs ?? false, context),
112
+ );
69
113
 
70
114
  if (!Array.isArray(items)) {
71
115
  throw new BadRequestException('forEach requires configuration.items array.');
72
116
  }
73
117
 
74
118
  const childNodes = context.node.tasks ?? [];
119
+ const iterationsByIndex: Record<number, any> = {};
120
+ const iterationsByKey: Record<string, any> = {};
121
+ const shouldAggregateByKey =
122
+ aggregateOutputs && configuration.outputKey !== undefined && configuration.outputKey !== null;
75
123
 
76
- for (let index = 0; index < items.length; index++) {
124
+ await this.runIterations(items, concurrencyLimit, async (item, index) => {
77
125
  const inheritedOutputs = context.outputs ?? {};
78
126
  const iterationOutputs = { ...inheritedOutputs };
79
-
80
- await context.runNodes(childNodes, {
81
- item: items[index],
127
+ const parentLoops = context.loops ?? [];
128
+ const currentLoop = {
129
+ item,
130
+ index,
131
+ nodeId: context.node.id,
132
+ stepExecutionKey: context.stepExecution.stepExecutionKey,
133
+ };
134
+ const loopContext = {
135
+ item,
82
136
  index,
137
+ parent: parentLoops[parentLoops.length - 1],
138
+ parents: [...parentLoops].reverse(),
139
+ loops: [...parentLoops, currentLoop],
140
+ };
141
+ const outputKey = shouldAggregateByKey
142
+ ? String(context.expression.interpolate(configuration.outputKey, {
143
+ ...context,
144
+ ...loopContext,
145
+ outputs: iterationOutputs,
146
+ }))
147
+ : undefined;
148
+
149
+ const childOutputs = await context.runNodes(childNodes, {
150
+ ...loopContext,
83
151
  outputs: iterationOutputs,
84
152
  });
85
- }
153
+
154
+ if (aggregateOutputs) {
155
+ const iterationResult = {
156
+ item,
157
+ index,
158
+ outputs: childOutputs,
159
+ };
160
+
161
+ iterationsByIndex[index] = iterationResult;
162
+ if (outputKey !== undefined) {
163
+ iterationsByKey[outputKey] = iterationResult;
164
+ }
165
+ }
166
+ });
86
167
 
87
168
  return {
88
169
  output: {
89
170
  count: items.length,
90
171
  childNodeIds: childNodes.map((childNode) => childNode.id),
172
+ concurrencyLimit,
91
173
  // Keep the control-node output intentionally small. Per-iteration
92
174
  // outputs are already stored on the child workflowStepExecution rows.
93
- iterationOutputsStoredOnSteps: true,
175
+ iterationOutputsStoredOnSteps: !aggregateOutputs,
176
+ ...(aggregateOutputs
177
+ ? {
178
+ iterations: {
179
+ byIndex: iterationsByIndex,
180
+ ...(shouldAggregateByKey
181
+ ? {
182
+ byKey: iterationsByKey,
183
+ }
184
+ : {}),
185
+ },
186
+ }
187
+ : {}),
94
188
  },
95
189
  };
96
190
  }
191
+
192
+ private resolveConcurrencyLimit(value: any): number {
193
+ const parsed = Number(value === '' || value === null || value === undefined ? 1 : value);
194
+ if (!Number.isFinite(parsed) || parsed < 0) {
195
+ throw new BadRequestException(
196
+ 'forEach configuration.concurrencyLimit must be 0 or a positive number.',
197
+ );
198
+ }
199
+
200
+ return Math.floor(parsed);
201
+ }
202
+
203
+ private async runIterations<T>(
204
+ items: T[],
205
+ concurrencyLimit: number,
206
+ handler: (item: T, index: number) => Promise<void>,
207
+ ): Promise<void> {
208
+ if (concurrencyLimit === 1 || items.length <= 1) {
209
+ for (let index = 0; index < items.length; index++) {
210
+ await handler(items[index], index);
211
+ }
212
+ return;
213
+ }
214
+
215
+ const limit = concurrencyLimit === 0 ? items.length : concurrencyLimit;
216
+ let nextIndex = 0;
217
+
218
+ const workers = Array.from(
219
+ { length: Math.min(limit, items.length) },
220
+ async () => {
221
+ while (nextIndex < items.length) {
222
+ const index = nextIndex;
223
+ nextIndex += 1;
224
+ await handler(items[index], index);
225
+ }
226
+ },
227
+ );
228
+
229
+ await Promise.all(workers);
230
+ }
97
231
  }
@@ -42,6 +42,13 @@ import {
42
42
  description: 'Nodes that run when the condition evaluates to false.',
43
43
  kind: 'sequence',
44
44
  },
45
+ {
46
+ key: 'errors',
47
+ label: 'Errors',
48
+ description: 'Nodes that run sequentially if the selected branch fails.',
49
+ kind: 'sequence',
50
+ layout: 'sequential',
51
+ },
45
52
  ],
46
53
  supportsDescription: true,
47
54
  supportsName: true,
@@ -162,6 +162,7 @@ import {
162
162
  description: 'The resolved log level.',
163
163
  valueType: 'string',
164
164
  path: 'level',
165
+ includeInRuntimeContext: false,
165
166
  },
166
167
  {
167
168
  key: 'message',
@@ -169,6 +170,7 @@ import {
169
170
  description: 'The rendered log message.',
170
171
  valueType: 'string',
171
172
  path: 'message',
173
+ includeInRuntimeContext: false,
172
174
  },
173
175
  ],
174
176
  supportsExpressions: true,