@@ -390,7 +390,11 @@ def copy_workflow_structure(
390390 target_client ,
391391 target_project_id : str ,
392392 ) -> "ProjectWorkflow" :
393- """Copy the workflow structure from a source workflow to a new project."""
393+ """Copy the workflow structure from a source workflow to a new project.
394+
395+ IMPORTANT: This method preserves existing initial node IDs in the target workflow
396+ to prevent workflow breakage. Only non-initial nodes get new IDs.
397+ """
394398 try :
395399 # Create a new workflow in the target project
396400 from labelbox .schema .workflow .workflow import ProjectWorkflow
@@ -399,38 +403,74 @@ def copy_workflow_structure(
399403 target_client , target_project_id
400404 )
401405
406+ # Find existing initial nodes in target workflow to preserve their IDs
407+ existing_initial_ids = {}
408+ for node_data in target_workflow .config .get ("nodes" , []):
409+ definition_id = node_data .get ("definitionId" )
410+ if (
411+ definition_id
412+ == WorkflowDefinitionId .InitialLabelingTask .value
413+ ):
414+ existing_initial_ids [
415+ WorkflowDefinitionId .InitialLabelingTask .value
416+ ] = node_data .get ("id" )
417+ elif (
418+ definition_id
419+ == WorkflowDefinitionId .InitialReworkTask .value
420+ ):
421+ existing_initial_ids [
422+ WorkflowDefinitionId .InitialReworkTask .value
423+ ] = node_data .get ("id" )
424+
402425 # Get the source config
403426 new_config = source_workflow .config .copy ()
404427 old_to_new_id_map = {}
405428
406- # Generate new IDs for all nodes
429+ # Generate new IDs for all nodes, but preserve existing initial node IDs
407430 if new_config .get ("nodes" ):
408- new_config ["nodes" ] = [
409- {
410- ** node ,
411- "id" : str (uuid .uuid4 ()),
412- }
413- for node in new_config ["nodes" ]
414- ]
415- # Create mapping of old to new IDs
416- old_to_new_id_map = {
417- old_node ["id" ]: new_node ["id" ]
418- for old_node , new_node in zip (
419- source_workflow .config ["nodes" ], new_config ["nodes" ]
431+ updated_nodes = []
432+ for node in new_config ["nodes" ]:
433+ definition_id = node .get ("definitionId" )
434+ old_id = node ["id" ]
435+
436+ # Preserve existing initial node IDs, generate new IDs for others
437+ if definition_id in existing_initial_ids :
438+ new_id = existing_initial_ids [definition_id ]
439+ else :
440+ new_id = str (uuid .uuid4 ())
441+
442+ old_to_new_id_map [old_id ] = new_id
443+ updated_nodes .append (
444+ {
445+ ** node ,
446+ "id" : new_id ,
447+ }
420448 )
421- }
449+
450+ new_config ["nodes" ] = updated_nodes
422451
423452 # Update edges to use the new node IDs
424453 if new_config .get ("edges" ):
425- new_config ["edges" ] = [
426- {
427- ** edge ,
428- "id" : str (uuid .uuid4 ()),
429- "source" : old_to_new_id_map [edge ["source" ]],
430- "target" : old_to_new_id_map [edge ["target" ]],
431- }
432- for edge in new_config ["edges" ]
433- ]
454+ updated_edges = []
455+ for edge in new_config ["edges" ]:
456+ source_id = old_to_new_id_map [edge ["source" ]]
457+ target_id = old_to_new_id_map [edge ["target" ]]
458+ source_handle = edge .get ("sourceHandle" , "if" )
459+ target_handle = edge .get ("targetHandle" , "in" )
460+
461+ # Generate edge ID using correct format: xy-edge__{source}{sourceHandle}-{target}{targetHandle}
462+ edge_id = f"xy-edge__{ source_id } { source_handle } -{ target_id } { target_handle } "
463+
464+ updated_edges .append (
465+ {
466+ ** edge ,
467+ "id" : edge_id ,
468+ "source" : source_id ,
469+ "target" : target_id ,
470+ }
471+ )
472+
473+ new_config ["edges" ] = updated_edges
434474
435475 # Update the target workflow with the new config
436476 target_workflow .config = new_config
@@ -450,8 +490,31 @@ def copy_from(
450490 source_workflow : "ProjectWorkflow" ,
451491 auto_layout : bool = True ,
452492 ) -> "ProjectWorkflow" :
453- """Copy the nodes and edges from a source workflow to this workflow."""
493+ """Copy the nodes and edges from a source workflow to this workflow.
494+
495+ IMPORTANT: This method preserves existing initial node IDs in the target workflow
496+ to prevent workflow breakage. Only non-initial nodes get new IDs.
497+ """
454498 try :
499+ # Find existing initial nodes in target workflow to preserve their IDs
500+ existing_initial_ids = {}
501+ for node_data in workflow .config .get ("nodes" , []):
502+ definition_id = node_data .get ("definitionId" )
503+ if (
504+ definition_id
505+ == WorkflowDefinitionId .InitialLabelingTask .value
506+ ):
507+ existing_initial_ids [
508+ WorkflowDefinitionId .InitialLabelingTask .value
509+ ] = node_data .get ("id" )
510+ elif (
511+ definition_id
512+ == WorkflowDefinitionId .InitialReworkTask .value
513+ ):
514+ existing_initial_ids [
515+ WorkflowDefinitionId .InitialReworkTask .value
516+ ] = node_data .get ("id" )
517+
455518 # Create a clean work config (without connections)
456519 work_config : Dict [str , List [Any ]] = {"nodes" : [], "edges" : []}
457520
@@ -463,9 +526,15 @@ def copy_from(
463526
464527 # First pass: Create all nodes by directly copying configuration
465528 for source_node_data in source_workflow .config .get ("nodes" , []):
466- # Generate a new ID for the node
467- new_id = f"node-{ uuid .uuid4 ()} "
529+ definition_id = source_node_data .get ("definitionId" )
468530 old_id = source_node_data .get ("id" )
531+
532+ # Preserve existing initial node IDs, generate new IDs for others
533+ if definition_id in existing_initial_ids :
534+ new_id = existing_initial_ids [definition_id ]
535+ else :
536+ new_id = f"node-{ uuid .uuid4 ()} "
537+
469538 id_mapping [old_id ] = new_id
470539
471540 # Create a new node data dictionary by copying the source node
@@ -498,12 +567,18 @@ def copy_from(
498567 continue
499568
500569 # Create new edge
570+ source_handle = source_edge_data .get ("sourceHandle" , "out" )
571+ target_handle = source_edge_data .get ("targetHandle" , "in" )
572+
573+ # Generate edge ID using correct format: xy-edge__{source}{sourceHandle}-{target}{targetHandle}
574+ edge_id = f"xy-edge__{ id_mapping [source_id ]} { source_handle } -{ id_mapping [target_id ]} { target_handle } "
575+
501576 new_edge = {
502- "id" : f"edge- { uuid . uuid4 () } " ,
577+ "id" : edge_id ,
503578 "source" : id_mapping [source_id ],
504579 "target" : id_mapping [target_id ],
505- "sourceHandle" : source_edge_data . get ( "sourceHandle" , "out" ) ,
506- "targetHandle" : source_edge_data . get ( "targetHandle" , "in" ) ,
580+ "sourceHandle" : source_handle ,
581+ "targetHandle" : target_handle ,
507582 }
508583
509584 # Add the edge to config
0 commit comments