Join
A Join task is used in conjunction with a Fork or Dynamic Fork task to wait on and join the forks. The Join task also aggregates the forked tasks' outputs for subsequent use.
The Join task's behavior varies based on the preceding fork type:
- When used with a Static Fork task, the Join task waits for a provided list of the forked tasks to be completed before proceeding with the next task.
- When used with a Dynamic Fork task, it implicitly waits for all the forked tasks to complete.
Task parameters
When used with a Static Fork, use these parameters in top level of the Join task configuration.
| Parameter | Type | Description | Required / Optional |
|---|---|---|---|
| joinOn | List[String] | (For Static Forks only) A list of task reference names that the Join task will wait for completion before proceeding with the next task. If not specified, the Join will move on to the next task without waiting for any forked tasks to complete. | Optional. |
| expression | String | The join script, which controls how the Join task completes if specified. | Optional. |
| evaluatorType | String | The language of the join script. Use graaljs, js, or javascript. The join script runs only when both evaluatorType and expression are set. |
Optional. |
When a Join task is used after a Switch task, the Join task can complete when the Switch task completes, even if the tasks within the switch branches have not finished. To handle this, add a task such as Set Variable at the end of the outermost Switch task and configure the Join task to join on that task. This ensures the Join task waits until all switch branch execution is complete before the workflow continues.
Join script configuration
A join script is optional and allows custom control over when the Join task completes. Script parameters can be passed through the Join task's input parameters. For predictable behavior, tasks referenced by the join script should be marked as optional.
Example join script:
(function(){
let results = {};
let pendingJoinsFound = false;
// $.joinOn is an array of task reference name strings, as configured in joinOn.
// $[element] is the task data object for that reference, containing status and output.
if($.joinOn){
$.joinOn.forEach((element)=>{
if($[element] && !['COMPLETED', 'COMPLETED_WITH_ERRORS'].includes($[element].status)){
results[element] = $[element].status;
pendingJoinsFound = true;
}
});
if(pendingJoinsFound){
return {
"status":"IN_PROGRESS",
"reasonForIncompletion":"Pending",
"outputData":{
"scriptResults": results
}
};
}
return true;
}
})();
In the example script, $.joinOn is an array of task reference name strings, matching the joinOn configuration. For each reference name, $[element] provides the corresponding task data object, including its status and output. The script checks whether each task to be joined has finished, meaning its status is COMPLETED or COMPLETED_WITH_ERRORS. An optional task that fails finishes with COMPLETED_WITH_ERRORS. If any task is still pending, the script returns the IN_PROGRESS status and the Join task keeps waiting. The example script can be modified to suit your use case.
JSON configuration
Here is the task configuration for a Join task.
With a static fork
{
"name": "join",
"taskReferenceName": "join_ref",
"inputParameters": {},
"type": "JOIN",
"joinOn": [
// List of task reference names that the join should wait for
],
"evaluatorType": "graaljs",
"expression": ""
}
With a dynamic fork
Output
The Join task will return a map of all completed forked task outputs (in other words, the output from all joinOn tasks.) The keys are task reference names of the tasks being joined and the values are the corresponding task outputs.
Example:
{
"taskReferenceName": {
"outputKey": "outputValue"
},
"anotherTaskReferenceName": {
"outputKey": "outputValue"
},
"someTaskReferenceName": {
"outputKey": "outputValue"
}
}
With a join script
If a join script is used, the Join task output is different. It contains joinOn, the list of task reference names being joined, and under each reference name the full task data of that task (such as its status, input, and output), not only its output. Any outputData returned by the script is also added to the Join task output.
{
"joinOn": [
"taskReferenceName",
"anotherTaskReferenceName"
],
"taskReferenceName": {
"referenceTaskName": "taskReferenceName",
"taskType": "SIMPLE",
"status": "COMPLETED",
"input": {},
"output": {
"outputKey": "outputValue"
}
},
"anotherTaskReferenceName": {
"referenceTaskName": "anotherTaskReferenceName",
"taskType": "SIMPLE",
"status": "COMPLETED",
"input": {},
"output": {
"outputKey": "outputValue"
}
}
}
Examples
Here are some examples for using the Join task.
Joining on all forks
In this example task configuration, the Join task will wait for the completion of tasks my_task_ref_1 and my_task_ref_2 as specified in joinOn.
[
{
"name": "fork_join",
"taskReferenceName": "my_fork_join_ref",
"type": "FORK_JOIN",
"forkTasks": [
[
{
"name": "my_task",
"taskReferenceName": "my_task_ref_1",
"type": "SIMPLE"
}
],
[
{
"name": "my_task",
"taskReferenceName": "my_task_ref_2",
"type": "SIMPLE"
}
]
]
},
{
"name": "join_task",
"taskReferenceName": "my_join_task_ref",
"type": "JOIN",
"joinOn": [
"my_task_ref_1",
"my_task_ref_2"
]
}
]
Ignoring one fork
In this example task configuration, the Fork task spawns three tasks: an email_notification task, a sms_notification task, and a http_notification task.
Email and SMS are usually best-effort delivery systems, while a HTTP-based notification can be retried until it succeeds or eventually gives up. Therefore, when you set up a notification workflow, you may decide to continue the workflow after you have kicked off an email and SMS notification, but let the http_notification task continue to execute without blocking the rest of the workflow.
In that case, you can specify the joinOn tasks as follows:
[
{
"name": "fork_join",
"taskReferenceName": "my_fork_join_ref",
"type": "FORK_JOIN",
"forkTasks": [
[
{
"name": "email_notification",
"taskReferenceName": "email_notification_ref",
"type": "SIMPLE"
}
],
[
{
"name": "sms_notification",
"taskReferenceName": "sms_notification_ref",
"type": "SIMPLE"
}
],
[
{
"name": "http_notification",
"taskReferenceName": "http_notification_ref",
"type": "SIMPLE"
}
]
]
},
{
"name": "notification_join",
"taskReferenceName": "notification_join_ref",
"type": "JOIN",
"joinOn": [
"email_notification_ref",
"sms_notification_ref"
]
}
]
Here is the output of notification_join. The output is a map, where the keys are the task reference names of the joinOn tasks, and the corresponding values are the outputs of those tasks.
{
"email_notification_ref": {
"email_sent_at": "2021-11-06T07:37:17+0000",
"email_sent_to": "test@example.com"
},
"sms_notification_ref": {
"sms_sent_at": "2021-11-06T07:37:17+0129",
"sms_sent_to": "+1-425-555-0189"
}
}
Using a join script
In this example, a Fork task runs two branches in parallel. One branch waits for 5 seconds and the other waits for 30 seconds. Both Wait tasks are marked as optional. The Join task uses a join script to control when the workflow can proceed, by explicitly checking the status of the forked tasks.
[
{
"name": "fork_tasks",
"taskReferenceName": "fork_tasks_ref",
"inputParameters": {},
"type": "FORK_JOIN",
"forkTasks": [
[
{
"name": "wait_short",
"taskReferenceName": "wait_short_ref",
"inputParameters": {
"duration": "5 seconds"
},
"type": "WAIT",
"optional": true
}
],
[
{
"name": "wait_long",
"taskReferenceName": "wait_long_ref",
"inputParameters": {
"duration": "30 seconds"
},
"type": "WAIT",
"optional": true
}
]
]
},
{
"name": "join_with_script",
"taskReferenceName": "join_with_script_ref",
"inputParameters": {},
"type": "JOIN",
"joinOn": [
"wait_short_ref",
"wait_long_ref"
],
"evaluatorType": "graaljs",
"expression": "(function(){\n let pending = false;\n let results = {};\n\n $.joinOn.forEach((ref) => {\n if ($[ref] && !['COMPLETED', 'COMPLETED_WITH_ERRORS'].includes($[ref].status)) {\n pending = true;\n results[ref] = $[ref].status;\n }\n });\n\n if (pending) {\n return {\n status: 'IN_PROGRESS',\n reasonForIncompletion: 'Waiting for forked tasks',\n outputData: {\n pendingTasks: results\n }\n };\n }\n\n return true;\n})();"
}
]
The join script is:
(function(){
let pending = false;
let results = {};
$.joinOn.forEach((ref) => {
if ($[ref] && !['COMPLETED', 'COMPLETED_WITH_ERRORS'].includes($[ref].status)) {
pending = true;
results[ref] = $[ref].status;
}
});
if (pending) {
return {
status: 'IN_PROGRESS',
reasonForIncompletion: 'Waiting for forked tasks',
outputData: {
pendingTasks: results
}
};
}
return true;
})();
This script completes the Join task only when all the forked tasks have finished, either COMPLETED or COMPLETED_WITH_ERRORS for an optional task that failed. If any forked task is still pending, the script returns the IN_PROGRESS status and the Join task keeps waiting.
When the workflow runs:
- The branch with the 5 second Wait task (
wait_short) completes first. - The Join task stays IN_PROGRESS while the 30 second Wait task is still running.
- After the 30 second Wait task completes, the join script returns true and the Join task completes.