@@ -390,7 +390,11 @@ def copy_workflow_structure(
390
390
target_client ,
391
391
target_project_id : str ,
392
392
) -> "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
+ """
394
398
try :
395
399
# Create a new workflow in the target project
396
400
from labelbox .schema .workflow .workflow import ProjectWorkflow
@@ -399,38 +403,74 @@ def copy_workflow_structure(
399
403
target_client , target_project_id
400
404
)
401
405
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
+
402
425
# Get the source config
403
426
new_config = source_workflow .config .copy ()
404
427
old_to_new_id_map = {}
405
428
406
- # Generate new IDs for all nodes
429
+ # Generate new IDs for all nodes, but preserve existing initial node IDs
407
430
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
+ }
420
448
)
421
- }
449
+
450
+ new_config ["nodes" ] = updated_nodes
422
451
423
452
# Update edges to use the new node IDs
424
453
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
434
474
435
475
# Update the target workflow with the new config
436
476
target_workflow .config = new_config
@@ -450,8 +490,31 @@ def copy_from(
450
490
source_workflow : "ProjectWorkflow" ,
451
491
auto_layout : bool = True ,
452
492
) -> "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
+ """
454
498
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
+
455
518
# Create a clean work config (without connections)
456
519
work_config : Dict [str , List [Any ]] = {"nodes" : [], "edges" : []}
457
520
@@ -463,9 +526,15 @@ def copy_from(
463
526
464
527
# First pass: Create all nodes by directly copying configuration
465
528
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" )
468
530
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
+
469
538
id_mapping [old_id ] = new_id
470
539
471
540
# Create a new node data dictionary by copying the source node
@@ -498,12 +567,18 @@ def copy_from(
498
567
continue
499
568
500
569
# 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
+
501
576
new_edge = {
502
- "id" : f"edge- { uuid . uuid4 () } " ,
577
+ "id" : edge_id ,
503
578
"source" : id_mapping [source_id ],
504
579
"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 ,
507
582
}
508
583
509
584
# Add the edge to config
0 commit comments