Skip to content

Commit 784f9aa

Browse files
committed
Feature: Workflow Split controllers now offer an optional "item filter" expression, to filter out specific items based on their properties, or array index, or other. Fixes #298.
1 parent 049beff commit 784f9aa

4 files changed

Lines changed: 56 additions & 5 deletions

File tree

docs/workflows.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,14 +80,28 @@ Controllers implement flow control. They generally have input and output poles a
8080

8181
#### Split Controller
8282

83-
The Split controller fans out work by taking an input list and launching one sub-job per item. Provide a dot-path to the list in the previous job context, such as `data.rows`. The engine resolves the path and expects an array; if the value is a string it is trimmed and split by newline. A special case is `files`, which splits the incoming files array so that each sub-job receives exactly one file.
83+
The Split controller fans out work by taking an input list and launching one sub-job per item. Provide an [expression](xyexp.md) to the list in the previous job context, such as `data.rows`. The engine resolves the path and expects an array; if the value is a string it is trimmed and split by newline. A special case is `files`, which splits the incoming files array so that each sub-job receives exactly one file.
8484

8585
In the UI, the split controller configuration dialog provides an "Expression Builder" button, which allows you to explore output data from recently completed jobs, and pick out a specific JSON key path to use for the expression string.
8686

8787
Each individual sub-job will receive one item from the split data. It will arrive in the job's [Job.input](data.md#job-input), either in `data` as a property named `item`, or as a [File](data.md#file) in the `files` array.
8888

8989
Split requires exactly one output connection to the Event or Job node it will run per item. Concurrency and queuing are governed by the limits attached to that node. After all items complete, you can continue the flow using a `continue` wire from the controlled node. The controller includes a "continue percentage" setting so you can require that at least N% of the sub-jobs succeed before continuing.
9090

91+
##### Split Item Filter
92+
93+
You can optionally filter out items from your split array, by including a special item filter expression. In this context use the special `item` keyword to refer to the item being filtered. If your expression evaluates to true, the item will be included in the set. Otherwise, it will be left out. Here is an example using a property inside the item:
94+
95+
```js
96+
item.random < 0.5
97+
```
98+
99+
Also available in context here is `index` (the 0-based index of the current item in the set), `workflow.params` (all workflow-level user fields), and `workflowData` (shared workflow data object). For example, here is how to filter out all odd items, and only keep the even ones, using the modulo operator:
100+
101+
```js
102+
index % 2 == 0
103+
```
104+
91105
#### Join Controller
92106

93107
The Join controller waits for multiple incoming flows to finish, then passes a combined result to the next step. You can wire multiple inputs into a Join; it initializes when the first input arrives and completes after all of its inputs have fired.

htdocs/js/pages/Workflows.class.js

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2131,6 +2131,20 @@ Page.Workflows = class Workflows extends Page.Events {
21312131
}) + '<div class="text_field_icon mdi mdi-database-search-outline" title="' + config.ui.tooltips.wfd_exp_builder + '" onClick="$P().openExpressionBuilder(this)"></div>'
21322132
});
21332133

2134+
// split filter
2135+
html += this.getFormRow({
2136+
id: 'd_wfd_split_filter',
2137+
content: this.getFormText({
2138+
id: 'fe_wfd_split_filter',
2139+
type: 'text',
2140+
spellcheck: 'false',
2141+
autocomplete: 'off',
2142+
maxlength: 8192,
2143+
class: 'monospace',
2144+
value: node.data.filter || ''
2145+
})
2146+
});
2147+
21342148
// if expression
21352149
html += this.getFormRow({
21362150
id: 'd_wfd_if',
@@ -2206,6 +2220,7 @@ Page.Workflows = class Workflows extends Page.Events {
22062220
case 'split':
22072221
node.data.split = $('#fe_wfd_split').val().trim();
22082222
if (!node.data.split.length) return app.badField('#fe_wfd_split');
2223+
node.data.filter = $('#fe_wfd_split_filter').val().trim();
22092224
node.data.continue = parseInt( $('#fe_wfd_continue').val() ) || 0;
22102225
break;
22112226

@@ -2260,14 +2275,14 @@ Page.Workflows = class Workflows extends Page.Events {
22602275
// handle type change
22612276
var do_change_type = function() {
22622277
// show/hide sections based on type
2263-
$('#d_wfd_stagger, #d_wfd_wait, #d_wfd_repeat, #d_wfd_split, #d_wfd_if, #d_wfd_title, #d_wfd_icon, #d_wfd_continue').hide();
2278+
$('#d_wfd_stagger, #d_wfd_wait, #d_wfd_repeat, #d_wfd_split, #d_wfd_split_filter, #d_wfd_if, #d_wfd_title, #d_wfd_icon, #d_wfd_continue').hide();
22642279

22652280
var type = $('#fe_wfd_type').val();
22662281
switch (type) {
22672282
case 'multiplex': $('#d_wfd_stagger, #d_wfd_continue').show(); break;
22682283
case 'wait': $('#d_wfd_wait').show(); break;
22692284
case 'repeat': $('#d_wfd_repeat, #d_wfd_continue').show(); break;
2270-
case 'split': $('#d_wfd_split, #d_wfd_continue').show(); break;
2285+
case 'split': $('#d_wfd_split, #d_wfd_split_filter, #d_wfd_continue').show(); break;
22712286
case 'decision': $('#d_wfd_if, #d_wfd_title, #d_wfd_icon').show(); break;
22722287
}
22732288

internal/ui.json

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -583,11 +583,15 @@
583583
},
584584
"d_wfd_split": {
585585
"label": "Split Expression:",
586-
"caption": "Specify the path to the array for splitting, using `dot.path.notation`. Use the special `files` keyword to split the input files."
586+
"caption": "Specify the path to the array for splitting, using [XYEXP](#Docs/xyexp), e.g. `data.items`. Use the special `files` keyword to split the input files."
587+
},
588+
"d_wfd_split_filter": {
589+
"label": "Item Filter:",
590+
"caption": "Optionally enter an expression to filter items, e.g. `item.random > 0.5`. If false, the item will be excluded from the set. [Learn More](#Docs/workflows/split-item-filter)"
587591
},
588592
"d_wfd_if": {
589593
"label": "Expression:",
590-
"caption": "Enter the expression to evaluate using dot.path.notation, e.g. `data.random > 0.5`. If true, control will pass onto the next node."
594+
"caption": "Enter the expression to evaluate using [XYEXP](#Docs/xyexp), e.g. `data.random > 0.5`. If true, control will pass onto the next node."
591595
},
592596
"d_wfd_title": {
593597
"label": "Custom Title:",

lib/workflow.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -653,6 +653,24 @@ class Workflows {
653653
return;
654654
}
655655

656+
// allow split controller to filter items
657+
if (node.data.filter) {
658+
try {
659+
value = value.filter( function(item, idx) {
660+
context.item = item;
661+
context.index = idx;
662+
return jexl.evalSync( node.data.filter, context );
663+
} );
664+
}
665+
catch (err) {
666+
this.logWorkflow(job, node.id, `WARNING: Failed to evaluate filter expression: {${node.data.filter}}: ${err}`);
667+
state.error = true;
668+
return;
669+
}
670+
delete context.item;
671+
delete context.index;
672+
}
673+
656674
// find single node we need to control
657675
var conns = Tools.findObjects( workflow.connections, { source: node.id } );
658676
if (!conns.length) {

0 commit comments

Comments
 (0)