@@ -8,6 +8,7 @@ import { createLogger } from '@/lib/logs/console/logger'
88import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
99import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom-tools-persistence'
1010import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
11+ import { isValidKey } from '@/lib/workflows/sanitization/key-validation'
1112import { validateWorkflowState } from '@/lib/workflows/sanitization/validation'
1213import { getAllBlocks , getBlock } from '@/blocks/registry'
1314import type { SubBlockConfig } from '@/blocks/types'
@@ -77,6 +78,7 @@ function logSkippedItem(skippedItems: SkippedItem[], item: SkippedItem): void {
7778 skippedItems . push ( item )
7879}
7980
81+
8082/**
8183 * Result of input validation
8284 */
@@ -850,13 +852,18 @@ function applyOperationsToWorkflowState(
850852 * Reorder operations to ensure correct execution sequence:
851853 * 1. delete - Remove blocks first to free up IDs and clean state
852854 * 2. extract_from_subflow - Extract blocks from subflows before modifications
853- * 3. add - Create new blocks so they exist before being referenced
855+ * 3. add - Create new blocks (sorted by connection dependencies)
854856 * 4. insert_into_subflow - Insert blocks into subflows (sorted by parent dependency)
855857 * 5. edit - Edit existing blocks last, so connections to newly added blocks work
856858 *
857- * This ordering is CRITICAL: edit operations may reference blocks being added
858- * in the same batch (e.g., connecting block A to newly added block B).
859- * Without proper ordering, the target block wouldn't exist yet.
859+ * This ordering is CRITICAL: operations may reference blocks being added/inserted
860+ * in the same batch. Without proper ordering, target blocks wouldn't exist yet.
861+ *
862+ * For add operations, we use a two-pass approach:
863+ * - Pass 1: Create all blocks (without connections)
864+ * - Pass 2: Add all connections (now all blocks exist)
865+ * This ensures that if block A connects to block B, and both are being added,
866+ * B will exist when we try to create the edge from A to B.
860867 */
861868 const deletes = operations . filter ( ( op ) => op . operation_type === 'delete' )
862869 const extracts = operations . filter ( ( op ) => op . operation_type === 'extract_from_subflow' )
@@ -868,6 +875,8 @@ function applyOperationsToWorkflowState(
868875 // This handles cases where a loop/parallel is being added along with its children
869876 const sortedInserts = topologicalSortInserts ( inserts , adds )
870877
878+ // We'll process add operations in two passes (handled in the switch statement below)
879+ // This is tracked via a separate flag to know which pass we're in
871880 const orderedOperations : EditWorkflowOperation [ ] = [
872881 ...deletes ,
873882 ...extracts ,
@@ -877,15 +886,46 @@ function applyOperationsToWorkflowState(
877886 ]
878887
879888 logger . info ( 'Operations after reordering:' , {
880- order : orderedOperations . map (
889+ totalOperations : orderedOperations . length ,
890+ deleteCount : deletes . length ,
891+ extractCount : extracts . length ,
892+ addCount : adds . length ,
893+ insertCount : sortedInserts . length ,
894+ editCount : edits . length ,
895+ operationOrder : orderedOperations . map (
881896 ( op ) =>
882897 `${ op . operation_type } :${ op . block_id } ${ op . params ?. subflowId ? `(parent:${ op . params . subflowId } )` : '' } `
883898 ) ,
884899 } )
885900
901+ // Two-pass processing for add operations:
902+ // Pass 1: Create all blocks (without connections)
903+ // Pass 2: Add all connections (all blocks now exist)
904+ const addOperationsWithConnections : Array < {
905+ blockId : string
906+ connections : Record < string , any >
907+ } > = [ ]
908+
886909 for ( const operation of orderedOperations ) {
887910 const { operation_type, block_id, params } = operation
888911
912+ // CRITICAL: Validate block_id is a valid string and not "undefined"
913+ // This prevents undefined keys from being set in the workflow state
914+ if ( ! isValidKey ( block_id ) ) {
915+ logSkippedItem ( skippedItems , {
916+ type : 'missing_required_params' ,
917+ operationType : operation_type ,
918+ blockId : String ( block_id || 'invalid' ) ,
919+ reason : `Invalid block_id "${ block_id } " (type: ${ typeof block_id } ) - operation skipped. Block IDs must be valid non-empty strings.` ,
920+ } )
921+ logger . error ( 'Invalid block_id detected in operation' , {
922+ operation_type,
923+ block_id,
924+ block_id_type : typeof block_id ,
925+ } )
926+ continue
927+ }
928+
889929 logger . debug ( `Executing operation: ${ operation_type } for block ${ block_id } ` , {
890930 params : params ? Object . keys ( params ) : [ ] ,
891931 currentBlockCount : Object . keys ( modifiedState . blocks ) . length ,
@@ -1128,6 +1168,22 @@ function applyOperationsToWorkflowState(
11281168
11291169 // Add new nested blocks
11301170 Object . entries ( params . nestedNodes ) . forEach ( ( [ childId , childBlock ] : [ string , any ] ) => {
1171+ // Validate childId is a valid string
1172+ if ( ! isValidKey ( childId ) ) {
1173+ logSkippedItem ( skippedItems , {
1174+ type : 'missing_required_params' ,
1175+ operationType : 'add_nested_node' ,
1176+ blockId : String ( childId || 'invalid' ) ,
1177+ reason : `Invalid childId "${ childId } " in nestedNodes - child block skipped` ,
1178+ } )
1179+ logger . error ( 'Invalid childId detected in nestedNodes' , {
1180+ parentBlockId : block_id ,
1181+ childId,
1182+ childId_type : typeof childId ,
1183+ } )
1184+ return
1185+ }
1186+
11311187 const childBlockState = createBlockFromParams (
11321188 childId ,
11331189 childBlock ,
@@ -1360,6 +1416,22 @@ function applyOperationsToWorkflowState(
13601416 // Handle nested nodes (for loops/parallels created from scratch)
13611417 if ( params . nestedNodes ) {
13621418 Object . entries ( params . nestedNodes ) . forEach ( ( [ childId , childBlock ] : [ string , any ] ) => {
1419+ // Validate childId is a valid string
1420+ if ( ! isValidKey ( childId ) ) {
1421+ logSkippedItem ( skippedItems , {
1422+ type : 'missing_required_params' ,
1423+ operationType : 'add_nested_node' ,
1424+ blockId : String ( childId || 'invalid' ) ,
1425+ reason : `Invalid childId "${ childId } " in nestedNodes - child block skipped` ,
1426+ } )
1427+ logger . error ( 'Invalid childId detected in nestedNodes' , {
1428+ parentBlockId : block_id ,
1429+ childId,
1430+ childId_type : typeof childId ,
1431+ } )
1432+ return
1433+ }
1434+
13631435 const childBlockState = createBlockFromParams (
13641436 childId ,
13651437 childBlock ,
@@ -1368,21 +1440,22 @@ function applyOperationsToWorkflowState(
13681440 )
13691441 modifiedState . blocks [ childId ] = childBlockState
13701442
1443+ // Defer connection processing to ensure all blocks exist first
13711444 if ( childBlock . connections ) {
1372- addConnectionsAsEdges (
1373- modifiedState ,
1374- childId ,
1375- childBlock . connections ,
1376- logger ,
1377- skippedItems
1378- )
1445+ addOperationsWithConnections . push ( {
1446+ blockId : childId ,
1447+ connections : childBlock . connections ,
1448+ } )
13791449 }
13801450 } )
13811451 }
13821452
1383- // Add connections as edges
1453+ // Defer connection processing to ensure all blocks exist first (pass 2)
13841454 if ( params . connections ) {
1385- addConnectionsAsEdges ( modifiedState , block_id , params . connections , logger , skippedItems )
1455+ addOperationsWithConnections . push ( {
1456+ blockId : block_id ,
1457+ connections : params . connections ,
1458+ } )
13861459 }
13871460 break
13881461 }
@@ -1506,13 +1579,18 @@ function applyOperationsToWorkflowState(
15061579 modifiedState . blocks [ block_id ] = newBlock
15071580 }
15081581
1509- // Add/update connections as edges
1582+ // Defer connection processing to ensure all blocks exist first
1583+ // This is particularly important when multiple blocks are being inserted
1584+ // and they have connections to each other
15101585 if ( params . connections ) {
1511- // Remove existing edges from this block
1586+ // Remove existing edges from this block first
15121587 modifiedState . edges = modifiedState . edges . filter ( ( edge : any ) => edge . source !== block_id )
15131588
1514- // Add new connections
1515- addConnectionsAsEdges ( modifiedState , block_id , params . connections , logger , skippedItems )
1589+ // Add to deferred connections list
1590+ addOperationsWithConnections . push ( {
1591+ blockId : block_id ,
1592+ connections : params . connections ,
1593+ } )
15161594 }
15171595 break
15181596 }
@@ -1562,6 +1640,34 @@ function applyOperationsToWorkflowState(
15621640 }
15631641 }
15641642
1643+ // Pass 2: Add all deferred connections from add/insert operations
1644+ // Now all blocks exist (from add, insert, and edit operations), so connections can be safely created
1645+ // This ensures that if block A connects to block B, and both are being added/inserted,
1646+ // B will exist when we create the edge from A to B
1647+ if ( addOperationsWithConnections . length > 0 ) {
1648+ logger . info ( 'Processing deferred connections from add/insert operations' , {
1649+ deferredConnectionCount : addOperationsWithConnections . length ,
1650+ totalBlocks : Object . keys ( modifiedState . blocks ) . length ,
1651+ } )
1652+
1653+ for ( const { blockId, connections } of addOperationsWithConnections ) {
1654+ // Verify the source block still exists (it might have been deleted by a later operation)
1655+ if ( ! modifiedState . blocks [ blockId ] ) {
1656+ logger . warn ( 'Source block no longer exists for deferred connection' , {
1657+ blockId,
1658+ availableBlocks : Object . keys ( modifiedState . blocks ) ,
1659+ } )
1660+ continue
1661+ }
1662+
1663+ addConnectionsAsEdges ( modifiedState , blockId , connections , logger , skippedItems )
1664+ }
1665+
1666+ logger . info ( 'Finished processing deferred connections' , {
1667+ totalEdges : modifiedState . edges . length ,
1668+ } )
1669+ }
1670+
15651671 // Regenerate loops and parallels after modifications
15661672 modifiedState . loops = generateLoopBlocks ( modifiedState . blocks )
15671673 modifiedState . parallels = generateParallelBlocks ( modifiedState . blocks )
0 commit comments