feat: add Parallel node for AgentFlow V2#6632
Conversation
Adds a new "Parallel" container node alongside the existing Iteration
node. Branches inside the box are auto-detected from the canvas graph
(each child with no incoming edge from a sibling becomes a branch
root), and all branches execute concurrently via Promise.allSettled
instead of sequentially.
- New Parallel.ts agentflow component (auto-discovered like Iteration)
- buildAgentflow.ts: branch discovery (constructGraphs/getStartingNode
reuse) + concurrent execution + per-branch state snapshot with a
diff-based merge so one branch's untouched keys don't clobber
another branch's update to the same key
- ParallelNode.jsx canvas container, registered in Canvas/Marketplace
canvases, drop/containment logic generalized for both container types
- Branch-aware execution log grouping in AgentExecutedDataCard/
ExecutionDetails ("Branch #N")
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a new 'Parallel' agentflow node to support concurrent branch execution, along with the corresponding backend execution logic, state merging, and UI components for visualization and resizing. The review feedback highlights several critical improvements: ensuring robust support for nested containers by merging 'iterationContext' and preserving 'parentNodeId', refining parent-matching logic in the UI tree-building, retrieving partial execution data from failed branches, and simplifying the 'ParallelNode' component's resizing implementation using percentage-based CSS dimensions.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| } else { | ||
| const errorMessage = getErrorMessage(settled.reason) | ||
| console.error(` ❌ Error in parallel branch ${branchIndex + 1}: ${errorMessage}`) | ||
| branchResults.push({ branchIndex, error: errorMessage }) | ||
| } |
There was a problem hiding this comment.
If a parallel branch fails, the promise is rejected, meaning none of the executed data from that branch is merged into the parent's execution data. Retrieve the partial execution data from the rejected promise's reason (Error object) to ensure failed branches are still correctly logged and visible in the UI.
} else {
const errorMessage = getErrorMessage(settled.reason)
console.error(` ❌ Error in parallel branch ${branchIndex + 1}: ${errorMessage}`)
branchResults.push({ branchIndex, error: errorMessage })
const branchExecutedData = (settled.reason as any)?.agentFlowExecutedData
if (branchExecutedData) {
const mappedData = branchExecutedData.map((data: IAgentflowExecutedData) => ({
...data,
data: {
...data.data,
branchIndex,
parentNodeId: reactFlowNode.data.id
}
}))
agentFlowExecutedData.push(...mappedData)
}
}| const branchExecutedData = subFlowResult.agentFlowExecutedData.map((data: IAgentflowExecutedData) => ({ | ||
| ...data, | ||
| data: { | ||
| ...data.data, | ||
| branchIndex, | ||
| parentNodeId: reactFlowNode.data.id | ||
| } | ||
| })) |
There was a problem hiding this comment.
Preserve the existing parentNodeId if it is already set to support nested containers correctly.
const branchExecutedData = subFlowResult.agentFlowExecutedData.map((data: IAgentflowExecutedData) => ({
...data,
data: {
...data.data,
branchIndex,
parentNodeId: data.data.parentNodeId || reactFlowNode.data.id
}
}))| iterationContext: { | ||
| branchIndex: branch.branchIndex, | ||
| sessionId, | ||
| agentflowRuntime: branchAgentflowRuntime | ||
| }, |
There was a problem hiding this comment.
Overwriting iterationContext completely breaks variable resolution (like $iteration) for nested parallel nodes inside iteration nodes. Merge the parent iterationContext instead of overwriting it.
iterationContext: {
...iterationContext,
branchIndex: branch.branchIndex,
sessionId,
agentflowRuntime: branchAgentflowRuntime
},| const sameGroup = | ||
| node.data?.iterationIndex !== undefined | ||
| ? potentialParent.data?.iterationIndex === node.data?.iterationIndex | ||
| : potentialParent.data?.branchIndex === node.data?.branchIndex |
There was a problem hiding this comment.
The parent matching logic only checks iterationIndex if defined, which can incorrectly match a parent from a different parallel branch in nested container scenarios. Check both indices if they are defined to ensure robust matching.
const sameGroup =
(node.data?.iterationIndex === undefined || potentialParent.data?.iterationIndex === node.data?.iterationIndex) &&
(node.data?.branchIndex === undefined || potentialParent.data?.branchIndex === node.data?.branchIndex)| const sameGroup = | ||
| node.data?.iterationIndex !== undefined | ||
| ? potentialParent.data?.iterationIndex === node.data?.iterationIndex | ||
| : potentialParent.data?.branchIndex === node.data?.branchIndex |
There was a problem hiding this comment.
Check both iterationIndex and branchIndex if defined to ensure robust parent matching in nested container scenarios.
const sameGroup =
(node.data?.iterationIndex === undefined || potentialParent.data?.iterationIndex === node.data?.iterationIndex) &&
(node.data?.branchIndex === undefined || potentialParent.data?.branchIndex === node.data?.branchIndex)| ) | ||
|
|
||
| return ( | ||
| <div ref={ref} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> |
There was a problem hiding this comment.
To enable smooth real-time resizing and simplify the code, set the outer container and card styles to fill 100% of the React Flow wrapper div. This eliminates the need for manual dimension tracking state, effects, and callbacks.
| <div ref={ref} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}> | |
| <div ref={ref} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} style={{ width: '100%', height: '100%' }}> |
| minHeight: Math.max(getMinimumHeight(), 250), | ||
| minWidth: 300, | ||
| width: cardDimensions.width, | ||
| height: cardDimensions.height, | ||
| backgroundColor: getBackgroundColor(), |
There was a problem hiding this comment.
Set width and height to 100% to automatically fill the wrapper div managed by React Flow.
| minHeight: Math.max(getMinimumHeight(), 250), | |
| minWidth: 300, | |
| width: cardDimensions.width, | |
| height: cardDimensions.height, | |
| backgroundColor: getBackgroundColor(), | |
| minHeight: Math.max(getMinimumHeight(), 250), | |
| minWidth: 300, | |
| width: '100%', | |
| height: '100%', | |
| backgroundColor: getBackgroundColor(), |
| <Box | ||
| sx={{ | ||
| height: `calc(${cardDimensions.height} - 20px)`, | ||
| width: `${cardDimensions.width}`, | ||
| overflow: 'hidden', |
There was a problem hiding this comment.
Summary
Promise.allSettled) instead of sequentially; a failing branch doesn't abort the othersChanges
packages/components/nodes/agentflow/Parallel/Parallel.ts— new structural component (no config inputs; branches come from canvas topology)packages/server/src/utils/buildAgentflow.ts— branch discovery (reuses existingconstructGraphs/getStartingNode), concurrent branch execution, and state mergepackages/ui/src/views/agentflowsv2/ParallelNode.jsx— new resizable canvas container nodepackages/ui/src/views/agentflowsv2/Canvas.jsx,MarketplaceCanvas.jsx— node type registration, drop/containment logic generalized to support both Iteration and Parallel containerspackages/ui/src/store/constant.js— icon/color registrationpackages/ui/src/views/chatmessage/AgentExecutedDataCard.jsx,packages/ui/src/views/agentexecutions/ExecutionDetails.jsx— execution log now groups parallel branch results ("Branch #N"), mirroring existing Iteration groupingTest plan
flowise-components,flowise(server), andflowise-uiall build/type-check cleanlyNodesPoolauto-discoversparallelAgentflowalongside the existing node setStart → start_time (Custom Function) → Parallel [5sec / 7sec / 10sec Custom Functions] → end_time (Custom Function) → Direct Reply1 2 3(all three branch values survived), Elapsed time:10036ms— i.e. total time ≈ the slowest branch (10s), not the sum (22s), confirming true concurrent executionBranch #1/Branch #2/Branch #3Screenshots
Added by PR author after opening this PR — canvas view of the test flow, Flow State config, one branch's Custom Function config, and the execution log showing the Branch #1/#2/#3 grouping with the elapsed-time result.
Related issue: #4673
🤖 Generated with Claude Code