Repository: Ikomia-dev/IkomiaApi (language: python)

Issue:
Add dlls to system path 



Discussion / hints:

Fix patch:
diff --git a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.datadictIO.DataDictIO.rst b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.datadictIO.DataDictIO.rst
index ee7d3c6b..8809fd17 100644
--- a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.datadictIO.DataDictIO.rst
+++ b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.datadictIO.DataDictIO.rst
@@ -13,18 +13,18 @@ DataDictIO
 
    .. rubric:: Overridden methods
    .. autosummary::
-      ~DataDictIO.clearData
-      ~DataDictIO.isDataAvailable
+      ~DataDictIO.clear_data
+      ~DataDictIO.is_data_available
       ~DataDictIO.load
       ~DataDictIO.save
-      ~DataDictIO.toJson
-      ~DataDictIO.fromJson
+      ~DataDictIO.to_json
+      ~DataDictIO.from_json
 
    .. rubric:: Details
    .. automethod:: __init__
-   .. automethod:: clearData
-   .. automethod:: fromJson
-   .. automethod:: isDataAvailable
+   .. automethod:: clear_data
+   .. automethod:: from_json
+   .. automethod:: is_data_available
    .. automethod:: load
    .. automethod:: save
-   .. automethod:: toJson
+   .. automethod:: to_json
diff --git a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CClassificationTask.rst b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CClassificationTask.rst
index d536bcd4..b7043a17 100644
--- a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CClassificationTask.rst
+++ b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CClassificationTask.rst
@@ -19,7 +19,7 @@ CClassificationTask
         ~CClassificationTask.get_names
         ~CClassificationTask.get_objects_results
         ~CClassificationTask.get_object_sub_image
-        ~CClassificationTask.get_visualization_image
+        ~CClassificationTask.get_image_with_graphics
         ~CClassificationTask.get_whole_image_results
         ~CClassificationTask.is_whole_image_classification
         ~CClassificationTask.read_class_names
@@ -90,7 +90,7 @@ CClassificationTask
     .. automethod:: get_names
     .. automethod:: get_objects_results
     .. automethod:: get_object_sub_image
-    .. automethod:: get_visualization_image
+    .. automethod:: get_image_with_graphics
     .. automethod:: get_whole_image_results
     .. automethod:: is_whole_image_classification
     .. automethod:: read_class_names
diff --git a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CInstanceSegmentationTask.rst b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CInstanceSegmentationTask.rst
index 52779eb0..07707399 100644
--- a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CInstanceSegmentationTask.rst
+++ b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CInstanceSegmentationTask.rst
@@ -17,7 +17,9 @@ CInstanceSegmentationTask
         ~CInstanceSegmentationTask.add_instance
         ~CInstanceSegmentationTask.get_names
         ~CInstanceSegmentationTask.get_results
-        ~CInstanceSegmentationTask.get_visualization_image
+        ~CInstanceSegmentationTask.get_image_with_graphics
+        ~CInstanceSegmentationTask.get_image_with_mask
+        ~CInstanceSegmentationTask.get_image_with_mask_and_graphics
         ~CInstanceSegmentationTask.read_class_names
         ~CInstanceSegmentationTask.set_colors
         ~CInstanceSegmentationTask.set_names
@@ -83,7 +85,9 @@ CInstanceSegmentationTask
     .. automethod:: add_instance
     .. automethod:: get_names
     .. automethod:: get_results
-    .. automethod:: get_visualization_image
+    .. automethod:: get_image_with_graphics
+    .. automethod:: get_image_with_mask
+    .. automethod:: get_image_with_mask_and_graphics
     .. automethod:: read_class_names
     .. automethod:: set_colors
     .. automethod:: set_names
diff --git a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CKeypointDetectionTask.rst b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CKeypointDetectionTask.rst
index de7ae559..1a12cbf9 100644
--- a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CKeypointDetectionTask.rst
+++ b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CKeypointDetectionTask.rst
@@ -19,7 +19,7 @@ CKeypointDetectionTask
         ~CKeypointDetectionTask.get_keypoint_names
         ~CKeypointDetectionTask.get_object_names
         ~CKeypointDetectionTask.get_results
-        ~CKeypointDetectionTask.get_visualization_image
+        ~CKeypointDetectionTask.get_image_with_graphics
         ~CKeypointDetectionTask.read_class_names
         ~CKeypointDetectionTask.set_keypoint_links
         ~CKeypointDetectionTask.set_keypoint_names
@@ -89,7 +89,7 @@ CKeypointDetectionTask
     .. automethod:: get_keypoint_names
     .. automethod:: get_object_names
     .. automethod:: get_results
-    .. automethod:: get_visualization_image
+    .. automethod:: get_image_with_graphics
     .. automethod:: read_class_names
     .. automethod:: set_keypoint_links
     .. automethod:: set_keypoint_names
diff --git a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CObjectDetectionTask.rst b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CObjectDetectionTask.rst
index 2c8a1c5f..64cf3fdb 100644
--- a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CObjectDetectionTask.rst
+++ b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CObjectDetectionTask.rst
@@ -17,7 +17,7 @@ CObjectDetectionTask
         ~CObjectDetectionTask.add_object
         ~CObjectDetectionTask.get_names
         ~CObjectDetectionTask.get_results
-        ~CObjectDetectionTask.get_visualization_image
+        ~CObjectDetectionTask.get_image_with_graphics
         ~CObjectDetectionTask.read_class_names
         ~CObjectDetectionTask.set_colors
         ~CObjectDetectionTask.set_names
@@ -83,7 +83,7 @@ CObjectDetectionTask
     .. automethod:: add_object
     .. automethod:: get_names
     .. automethod:: get_results
-    .. automethod:: get_visualization_image
+    .. automethod:: get_image_with_graphics
     .. automethod:: read_class_names
     .. automethod:: set_colors
     .. automethod:: set_names
diff --git a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CSemanticSegmentationTask.rst b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CSemanticSegmentationTask.rst
index ff56a7ee..db6ff4dc 100644
--- a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CSemanticSegmentationTask.rst
+++ b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.pydataprocess.CSemanticSegmentationTask.rst
@@ -16,7 +16,7 @@ CSemanticSegmentationTask
         ~CSemanticSegmentationTask.__init__
         ~CSemanticSegmentationTask.get_names
         ~CSemanticSegmentationTask.get_results
-        ~CSemanticSegmentationTask.get_visualization_image
+        ~CSemanticSegmentationTask.get_image_with_mask
         ~CSemanticSegmentationTask.read_class_names
         ~CSemanticSegmentationTask.set_colors
         ~CSemanticSegmentationTask.set_names
@@ -82,7 +82,7 @@ CSemanticSegmentationTask
     .. automethod:: __init__
     .. automethod:: get_names
     .. automethod:: get_results
-    .. automethod:: get_visualization_image
+    .. automethod:: get_image_with_mask
     .. automethod:: read_class_names
     .. automethod:: set_colors
     .. automethod:: set_names
diff --git a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.workflow.Workflow.rst b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.workflow.Workflow.rst
index f24cb7ea..ea3bd8b1 100644
--- a/doc/source/_autosummary/_autosummary/ikomia.dataprocess.workflow.Workflow.rst
+++ b/doc/source/_autosummary/_autosummary/ikomia.dataprocess.workflow.Workflow.rst
@@ -18,21 +18,13 @@ Workflow
         ~Workflow.add_task
         ~Workflow.connect_tasks
         ~Workflow.find_task
-        ~Workflow.get_array_output
-        ~Workflow.get_blob_measure_output
-        ~Workflow.get_dataset_output
-        ~Workflow.get_data_string_output
-        ~Workflow.get_graphics_output
-        ~Workflow.get_image
-        ~Workflow.get_image_output
-        ~Workflow.get_image_with_graphics
-        ~Workflow.get_instance_segmentation_output
-        ~Workflow.get_numeric_output
-        ~Workflow.get_object_detection_output
-        ~Workflow.get_path_output
-        ~Workflow.get_semantic_segmentation_output
+        ~Workflow.get_task_id
+        ~Workflow.get_task_output
         ~Workflow.get_tasks
         ~Workflow.get_time_metrics
+        ~Workflow.load
+        ~Workflow.remove_task
+        ~Workflow.root
         ~Workflow.run
         ~Workflow.run_on
         ~Workflow.set_directory_input
@@ -91,38 +83,29 @@ Workflow
     .. automethod:: export_graphviz
     .. automethod:: find_task
     .. automethod:: get_children
-    .. automethod:: get_array_output
-    .. automethod:: get_blob_measure_output
-    .. automethod:: get_dataset_output
-    .. automethod:: get_data_string_output
     .. automethod:: get_edge_info
     .. automethod:: get_edge_source
     .. automethod:: get_edge_target
     .. automethod:: get_elapsed_time_to
     .. automethod:: get_final_tasks
-    .. automethod:: get_graphics_output
-    .. automethod:: get_image
-    .. automethod:: get_image_output
-    .. automethod:: get_image_with_graphics
     .. automethod:: get_in_edges
-    .. automethod:: get_instance_segmentation_output
-    .. automethod:: get_numeric_output
-    .. automethod:: get_object_detection_output
     .. automethod:: get_out_edges
     .. automethod:: get_parents
-    .. automethod:: get_path_output
     .. automethod:: get_required_tasks
     .. automethod:: get_root_id
     .. automethod:: get_root_target_types
-    .. automethod:: get_semantic_segmentation_output
     .. automethod:: get_task
+    .. automethod:: get_task_id
     .. automethod:: get_task_count
     .. automethod:: get_task_ids
+    .. automethod:: get_task_output
     .. automethod:: get_tasks
     .. automethod:: get_time_metrics
     .. automethod:: get_total_elapsed_time
     .. automethod:: load
     .. automethod:: remove_input
+    .. automethod:: remove_task
+    .. automethod:: root
     .. automethod:: run
     .. automethod:: run_on
     .. automethod:: set_auto_save
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.core.pycore.CWorkflowTaskIO.rst b/doc/source/_autosummary/_autosummaryv/ikomia.core.pycore.CWorkflowTaskIO.rst
index c850efbb..42c7cb17 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.core.pycore.CWorkflowTaskIO.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.core.pycore.CWorkflowTaskIO.rst
@@ -18,7 +18,6 @@ CWorkflowTaskIO
         ~CWorkflowTaskIO.copy_static_data
         ~CWorkflowTaskIO.from_json
         ~CWorkflowTaskIO.get_unit_element_count
-        ~CWorkflowTaskIO.is_auto_input
         ~CWorkflowTaskIO.is_composite
         ~CWorkflowTaskIO.is_data_available
         ~CWorkflowTaskIO.load
@@ -45,7 +44,6 @@ CWorkflowTaskIO
     .. automethod:: copy_static_data
     .. automethod:: from_json
     .. automethod:: get_unit_element_count
-    .. automethod:: is_auto_input
     .. automethod:: is_composite
     .. automethod:: is_data_available
     .. automethod:: load
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CArrayIO.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CArrayIO.rst
index 3db57460..087a849e 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CArrayIO.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CArrayIO.rst
@@ -26,7 +26,6 @@ CArrayIO
     .. rubric:: Inherited methods
     .. autosummary::
         ~CArrayIO.copy_static_data
-        ~CArrayIO.is_auto_input
 
     .. rubric:: Attributes
     .. autosummary::
@@ -44,7 +43,6 @@ CArrayIO
     .. automethod:: copy_static_data
     .. automethod:: get_array
     .. automethod:: get_unit_element_count
-    .. automethod:: is_auto_input
     .. automethod:: is_data_available
     .. automethod:: set_array
    
\ No newline at end of file
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CBlobMeasureIO.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CBlobMeasureIO.rst
index 4ca84b98..8f25f2b5 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CBlobMeasureIO.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CBlobMeasureIO.rst
@@ -26,10 +26,6 @@ CBlobMeasureIO
         ~CBlobMeasureIO.is_data_available
         ~CBlobMeasureIO.to_json
 
-    .. rubric:: Inherited methods
-    .. autosummary::
-        ~CBlobMeasureIO.is_auto_input
-
     .. rubric:: Attributes
     .. autosummary::
         ~CBlobMeasureIO.auto_save
@@ -49,5 +45,4 @@ CBlobMeasureIO
    .. automethod:: set_object_measure
    .. automethod:: clear_data
    .. automethod:: is_data_available
-   .. automethod:: is_auto_input
    .. automethod:: to_json
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CDataStringIO.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CDataStringIO.rst
index 650b82dc..987d3645 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CDataStringIO.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CDataStringIO.rst
@@ -38,7 +38,6 @@ String specialization of C++ template class CNumericIO.
     .. autosummary::
         ~CNumericIO.copy_static_data
         ~CNumericIO.get_unit_element_count
-        ~CNumericIO.is_auto_input
 
     .. rubric:: Attributes
     .. autosummary::
@@ -63,7 +62,6 @@ String specialization of C++ template class CNumericIO.
    .. automethod:: get_plot_type
    .. automethod:: get_unit_element_count
    .. automethod:: get_value_list
-   .. automethod:: is_auto_input
    .. automethod:: is_data_available
    .. automethod:: load
    .. automethod:: save
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CDatasetIO.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CDatasetIO.rst
index b565d747..5e5e32b4 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CDatasetIO.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CDatasetIO.rst
@@ -27,7 +27,6 @@ CDatasetIO
     .. autosummary::
         ~CDatasetIO.clear_data
         ~CDatasetIO.from_json
-        ~CDatasetIO.is_auto_input
         ~CDatasetIO.is_data_available
         ~CDatasetIO.save
         ~CDatasetIO.to_json
@@ -58,7 +57,6 @@ CDatasetIO
    .. automethod:: get_image_paths
    .. automethod:: get_mask_path
    .. automethod:: get_unit_element_count
-   .. automethod:: is_auto_input
    .. automethod:: is_data_available
    .. automethod:: save
    .. automethod:: to_json
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CGraphicsInput.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CGraphicsInput.rst
index f0396a43..89d0ff3c 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CGraphicsInput.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CGraphicsInput.rst
@@ -30,7 +30,6 @@ CGraphicsInput
     .. autosummary::
         ~CGraphicsInput.copy_static_data
         ~CGraphicsInput.get_unit_element_count
-        ~CGraphicsInput.is_auto_input
    
     .. rubric:: Attributes
     .. autosummary::
@@ -49,7 +48,6 @@ CGraphicsInput
    .. automethod:: from_json
    .. automethod:: get_items
    .. automethod:: get_unit_element_count
-   .. automethod:: is_auto_input
    .. automethod:: is_data_available
    .. automethod:: load
    .. automethod:: save
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CGraphicsOutput.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CGraphicsOutput.rst
index 95c168ce..2e419f05 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CGraphicsOutput.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CGraphicsOutput.rst
@@ -40,7 +40,6 @@ CGraphicsOutput
     .. autosummary::
         ~CGraphicsOutput.copy_static_data
         ~CGraphicsOutput.get_unit_element_count
-        ~CGraphicsOutput.is_auto_input
 
     .. rubric:: Attributes
     .. autosummary::
@@ -65,12 +64,13 @@ CGraphicsOutput
    .. automethod:: clear_data
    .. automethod:: copy_static_data
    .. automethod:: from_json
+   .. automethod:: get_items
    .. automethod:: get_image_index
    .. automethod:: get_unit_element_count
-   .. automethod:: is_auto_input
    .. automethod:: is_data_available
    .. automethod:: load
    .. automethod:: save
+   .. automethod:: set_items
    .. automethod:: set_image_index
    .. automethod:: set_new_layer
    .. automethod:: to_json
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CImageIO.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CImageIO.rst
index 92de623d..03ae06c8 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CImageIO.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CImageIO.rst
@@ -36,10 +36,6 @@ CImageIO
         ~CImageIO.save
         ~CImageIO.to_json
         ~CImageIO.from_json
-
-    .. rubric:: Inherited methods
-    .. autosummary::
-        ~CImageIO.is_auto_input
    
     .. rubric:: Attributes
     .. autosummary::
@@ -63,7 +59,6 @@ CImageIO
    .. automethod:: get_image_with_graphics
    .. automethod:: get_overlay_mask
    .. automethod:: get_unit_element_count
-   .. automethod:: is_auto_input
    .. automethod:: is_data_available
    .. automethod:: is_overlay_available
    .. automethod:: load
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CInstanceSegmentationIO.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CInstanceSegmentationIO.rst
index 8f878a80..bb34dda8 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CInstanceSegmentationIO.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CInstanceSegmentationIO.rst
@@ -33,7 +33,6 @@ CInstanceSegmentationIO
     .. autosummary::
         ~CInstanceSegmentationIO.copy_static_data
         ~CInstanceSegmentationIO.get_unit_element_count
-        ~CInstanceSegmentationIO.is_auto_input
 
     .. rubric:: Attributes
     .. autosummary::
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CKeypointsIO.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CKeypointsIO.rst
index 3cea1ada..b858b667 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CKeypointsIO.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CKeypointsIO.rst
@@ -36,7 +36,6 @@ CKeypointsIO
     .. autosummary::
         ~CKeypointsIO.copy_static_data
         ~CKeypointsIO.get_unit_element_count
-        ~CKeypointsIO.is_auto_input
 
     .. rubric:: Attributes
     .. autosummary::
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CNumericIO.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CNumericIO.rst
index a9008db3..4ece423e 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CNumericIO.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CNumericIO.rst
@@ -38,7 +38,6 @@ Float specialization of C++ template class CNumericIO.
     .. autosummary::
         ~CNumericIO.copy_static_data
         ~CNumericIO.get_unit_element_count
-        ~CNumericIO.is_auto_input
 
     .. rubric:: Attributes
     .. autosummary::
@@ -63,7 +62,6 @@ Float specialization of C++ template class CNumericIO.
    .. automethod:: get_plot_type
    .. automethod:: get_unit_element_count
    .. automethod:: get_value_list
-   .. automethod:: is_auto_input
    .. automethod:: is_data_available
    .. automethod:: load
    .. automethod:: save
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CObjectDetectionIO.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CObjectDetectionIO.rst
index b7c98c68..ada3198e 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CObjectDetectionIO.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CObjectDetectionIO.rst
@@ -32,7 +32,6 @@ CObjectDetectionIO
     .. autosummary::
         ~CObjectDetectionIO.copy_static_data
         ~CObjectDetectionIO.get_unit_element_count
-        ~CObjectDetectionIO.is_auto_input
 
     .. rubric:: Attributes
     .. autosummary::
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CPathIO.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CPathIO.rst
index b30d868e..a1539957 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CPathIO.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CPathIO.rst
@@ -19,7 +19,6 @@ CPathIO
     .. rubric:: Overridden methods
     .. autosummary::
         ~CPathIO.clear_data
-        ~CPathIO.is_auto_input
         ~CPathIO.is_data_available
 
     .. rubric:: Inherited methods
@@ -43,6 +42,5 @@ CPathIO
    .. automethod:: copy_static_data
    .. automethod:: get_path
    .. automethod:: get_unit_element_count
-   .. automethod:: is_auto_input
    .. automethod:: is_data_available
    .. automethod:: set_path
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CSemanticSegmentationIO.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CSemanticSegmentationIO.rst
index eee0d317..a1e1e0a6 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CSemanticSegmentationIO.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CSemanticSegmentationIO.rst
@@ -16,23 +16,23 @@ CSemanticSegmentationIO
         ~CSemanticSegmentationIO.get_mask
         ~CSemanticSegmentationIO.get_class_names
         ~CSemanticSegmentationIO.get_colors
-        ~CSemanticSegmentationIO.set_mask
+        ~CSemanticSegmentationIO.set_class_colors
         ~CSemanticSegmentationIO.set_class_names
+        ~CSemanticSegmentationIO.set_mask
 
     .. rubric:: Overridden methods
     .. autosummary::
         ~CSemanticSegmentationIO.clear_data
+        ~CSemanticSegmentationIO.from_json
         ~CSemanticSegmentationIO.is_data_available
         ~CSemanticSegmentationIO.load
         ~CSemanticSegmentationIO.save
         ~CSemanticSegmentationIO.to_json
-        ~CSemanticSegmentationIO.from_json
 
     .. rubric:: Inherited methods
     .. autosummary::
         ~CSemanticSegmentationIO.copy_static_data
         ~CSemanticSegmentationIO.get_unit_element_count
-        ~CSemanticSegmentationIO.is_auto_input
 
     .. rubric:: Attributes
     .. autosummary::
@@ -46,13 +46,14 @@ CSemanticSegmentationIO
 
     .. rubric:: Details
     .. automethod:: __init__
+    .. automethod:: from_json
     .. automethod:: get_mask
     .. automethod:: get_class_names
     .. automethod:: get_colors
-    .. automethod:: set_mask
-    .. automethod:: set_class_names
     .. automethod:: is_data_available
     .. automethod:: load
     .. automethod:: save
-    .. automethod:: to_json
-    .. automethod:: from_json
\ No newline at end of file
+    .. automethod:: set_class_names
+    .. automethod:: set_class_colors
+    .. automethod:: set_mask
+    .. automethod:: to_json
\ No newline at end of file
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CTextIO.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CTextIO.rst
index 2dd5a95a..da6bc73e 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CTextIO.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CTextIO.rst
@@ -34,7 +34,6 @@ CTextIO
     .. autosummary::
         ~CTextIO.copy_static_data
         ~CTextIO.get_unit_element_count
-        ~CTextIO.is_auto_input
 
     .. rubric:: Attributes
     .. autosummary::
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CVideoIO.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CVideoIO.rst
index 1e5ec54e..701fca4f 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CVideoIO.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CVideoIO.rst
@@ -43,7 +43,6 @@ CVideoIO
         ~CVideoIO.get_channel_count
         ~CVideoIO.get_data
         ~CVideoIO.get_overlay_mask
-        ~CVideoIO.is_auto_input
         ~CVideoIO.is_overlay_available
         ~CVideoIO.set_channel_count
         ~CVideoIO.set_current_image
diff --git a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CWidgetOutput.rst b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CWidgetOutput.rst
index da742a7e..947f1f51 100644
--- a/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CWidgetOutput.rst
+++ b/doc/source/_autosummary/_autosummaryv/ikomia.dataprocess.pydataprocess.CWidgetOutput.rst
@@ -24,7 +24,6 @@ CWidgetOutput
     .. autosummary::
         ~CWidgetOutput.copy_static_data
         ~CWidgetOutput.get_unit_element_count
-        ~CWidgetOutput.is_auto_input
 
     .. rubric:: Attributes
     .. autosummary::
@@ -41,7 +40,6 @@ CWidgetOutput
    .. automethod:: clear_data
    .. automethod:: copy_static_data
    .. automethod:: get_unit_element_count
-   .. automethod:: is_auto_input
    .. automethod:: is_data_available
    .. automethod:: set_widget
    
\ No newline at end of file
diff --git a/doc/source/_autosummary/ikomia.core.task.rst b/doc/source/_autosummary/ikomia.core.task.rst
index 3ae42199..5e4edcd6 100644
--- a/doc/source/_autosummary/ikomia.core.task.rst
+++ b/doc/source/_autosummary/ikomia.core.task.rst
@@ -20,31 +20,9 @@
     .. autosummary::
 
         create
-        get_array_output
-        get_blob_measure_output
-        get_dataset_output
-        get_data_string_output
-        get_graphics_output
-        get_image_output
-        get_image_with_graphics
-        get_instance_segmentation_output
-        get_numeric_output
-        get_object_detection_output
-        get_path_output
-        get_semantic_segmentation_output
+        get_output
 
     .. rubric:: Details
 
     .. autofunction:: create
-    .. autofunction:: get_array_output
-    .. autofunction:: get_blob_measure_output
-    .. autofunction:: get_dataset_output
-    .. autofunction:: get_data_string_output
-    .. autofunction:: get_graphics_output
-    .. autofunction:: get_image_output
-    .. autofunction:: get_image_with_graphics
-    .. autofunction:: get_instance_segmentation_output
-    .. autofunction:: get_numeric_output
-    .. autofunction:: get_object_detection_output
-    .. autofunction:: get_path_output
-    .. autofunction:: get_semantic_segmentation_output
\ No newline at end of file
+    .. autofunction:: get_output
\ No newline at end of file
diff --git a/doc/source/_autosummary/ikomia.dataprocess.displayIO.rst b/doc/source/_autosummary/ikomia.utils.displayIO.rst
similarity index 76%
rename from doc/source/_autosummary/ikomia.dataprocess.displayIO.rst
rename to doc/source/_autosummary/ikomia.utils.displayIO.rst
index 89e6c2d3..475ea494 100644
--- a/doc/source/_autosummary/ikomia.dataprocess.displayIO.rst
+++ b/doc/source/_autosummary/ikomia.utils.displayIO.rst
@@ -1,7 +1,7 @@
 ﻿DisplayIO
 =========
 
-.. automodule:: ikomia.dataprocess.displayIO
+.. automodule:: ikomia.utils.displayIO
 
     .. rubric:: Functions
     .. autosummary::
diff --git a/doc/source/conf.py b/doc/source/conf.py
index cea9c590..0578823a 100644
--- a/doc/source/conf.py
+++ b/doc/source/conf.py
@@ -21,7 +21,7 @@
 author = 'Ludovic Barusseau'
 
 # The full version, including alpha/beta/rc tags
-version = '0.8.1'
+version = '0.9.0'
 
 
 # -- General configuration ---------------------------------------------------
diff --git a/doc/source/modules.rst b/doc/source/modules.rst
index c82f385f..92fbb199 100644
--- a/doc/source/modules.rst
+++ b/doc/source/modules.rst
@@ -13,7 +13,7 @@ the C capital letter.
     ikomia.utils.pyutils
     ikomia.utils.pyqtutils
     ikomia.utils.qtconversion
-
+    ikomia.utils.displayIO
 
 
 .. rubric:: Core module
@@ -32,7 +32,6 @@ the C capital letter.
     ikomia.dataprocess.registry
     ikomia.dataprocess.workflow
     ikomia.dataprocess.datadictIO
-    ikomia.dataprocess.displayIO
 
 
 .. rubric:: DNN module
diff --git a/ikomia/__init__.py b/ikomia/__init__.py
index cbc64c51..5d6fc763 100644
--- a/ikomia/__init__.py
+++ b/ikomia/__init__.py
@@ -37,7 +37,7 @@
 
 
 global ik_api_session
-ik_api_session = None
+ik_api_session = LoginSession()
 
 # --------------------------------------
 # ----- Ikomia algorithms registry -----
@@ -63,9 +63,4 @@ def authenticate():
     username = os.environ.get("IKOMIA_USER")
     pwd = os.environ.get("IKOMIA_PWD")
     global ik_api_session
-    # check if authentication has be made before
-    not_auth = ik_api_session is None or ik_api_session.token is None
-    ik_api_session = LoginSession(username, pwd)
-
-    if config.main_cfg["registry"]["auto_completion"]:
-        autocomplete.make_online_plugins(force=not_auth)
+    ik_api_session.authenticate(username, pwd)
diff --git a/ikomia/core/auth.py b/ikomia/core/auth.py
index 88bbe454..ec3da9e7 100644
--- a/ikomia/core/auth.py
+++ b/ikomia/core/auth.py
@@ -21,12 +21,14 @@
 
 
 class LoginSession:
-    def __init__(self, username, pwd):
+    def __init__(self, username=None, pwd=None):
         self.session = requests.Session()
         self.token = None
-        self.authenticate(username, pwd)
 
-    @http.http_except
+        if username is not None and pwd is not None:
+            self.authenticate(username, pwd)
+
+    @http.http_no_raise
     def authenticate(self, username, pwd):
         url = config.main_cfg["hub"]["url"] + "/api/rest-auth/login/"
         data = {"username": username, "password": pwd}
diff --git a/ikomia/core/task.py b/ikomia/core/task.py
index 9ebe8593..6d647c6a 100644
--- a/ikomia/core/task.py
+++ b/ikomia/core/task.py
@@ -72,235 +72,31 @@ def create(name=""):
     return ikomia.ik_registry.create_algorithm(name)
 
 
-def _get_outputs(task, types, target_class=None, index=-1):
-    if task is None:
-        raise RuntimeError("Cannot get outputs from None task.")
+def get_output(task_obj, types, index=-1):
+    """
+    Get specific output(s) of a task from the given types (:py:class:`~ikomia.core.pycore.IODataType`).
+
+    Args:
+        task_obj (:py:class:`~ikomia.core.pycore.CWorkflowTask` based object): task instance
+        types (list of :py:class:`~ikomia.core.pycore.IODataType`): output data types
+        index (int): zero-based index in the candidates output list. If -1, all candidates are returned.
+
+    Returns:
+        instance or list of :py:class:`~ikomia.dataprocess.pydataprocess.CWorkflowTaskIO` based object: outputs
+    """
+    if task_obj is None:
+        raise RuntimeError("Cannot get outputs from None object.")
 
     outputs = []
-    all_outputs = task.get_outputs()
+    all_outputs = task_obj.get_outputs()
 
-    # TODO: manage composite I/O
     for output in all_outputs:
         if output.data_type in types:
-            if target_class is None:
-                outputs.append(output)
-            elif type(output) == target_class:
-                outputs.append(output)
+            outputs.append(output)
 
     if index == -1:
         return outputs
     elif 0 <= index < len(outputs):
         return outputs[index]
     else:
-        return None
-
-
-def get_image_output(task, index=-1):
-    """
-    Get IMAGE output(s) of the given task. A task can have multiple IMAGE outputs so the index
-    argument can be set to specify the wanted output.
-
-    Args:
-        task (:py:class:`~ikomia.core.pycore.CWorkflowTask` or derived): object instance.
-        index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-    Returns:
-        :py:class:`~ikomia.dataprocess.pydataprocess.CImageIO`: output if index is set, list of outputs otherwise.
-    """
-    data_types = [core.IODataType.IMAGE, core.IODataType.IMAGE_BINARY, core.IODataType.IMAGE_LABEL]
-    return _get_outputs(task, data_types, index=index)
-
-
-def get_image_with_graphics(task, image_index=0, graphics_index=0):
-    """
-    Get image (numpy array) of a specific IMAGE output with graphics items from a specific GRAPHICS output burnt
-    into it. Index of IMAGE and GRAPHICS outputs must be given.
-
-    Args:
-        task (:py:class:`~ikomia.core.pycore.CWorkflowTask` or derived): object instance.
-        image_index (int): zero-based index of the IMAGE output.
-        graphics_index (int): zero-based index of the GRAPHICS output.
-
-    Returns:
-        Numpy array: result image.
-    """
-    if image_index < 0 or graphics_index < 0:
-        raise RuntimeError("Output index must be equal or greater than 0.")
-
-    img_data_types = [core.IODataType.IMAGE, core.IODataType.IMAGE_BINARY, core.IODataType.IMAGE_LABEL]
-    img_output = _get_outputs(task, img_data_types, index=image_index)
-    if img_output is None:
-        logger.error(f"No image output available at index {image_index} in task {task.name}")
-        return None
-
-    graphics_output = _get_outputs(task, [core.IODataType.OUTPUT_GRAPHICS])
-    composite_output = _get_outputs(task, [core.IODataType.OBJECT_DETECTION, core.IODataType.INSTANCE_SEGMENTATION])
-
-    for out in composite_output:
-        graphics_output.append(out.getGraphicsIO())
-
-    if graphics_index < len(graphics_output):
-        return img_output.get_image_with_graphics(graphics_output[graphics_index])
-    else:
-        logger.error(f"No graphics output available at index {graphics_index} for task {task.name}")
-        return None
-
-
-def get_graphics_output(task, index=-1):
-    """
-    Get GRAPHICS output(s) of the given task. A task can have multiple GRAPHICS outputs so the index
-    argument can be set to specify the wanted output.
-
-    Args:
-        task (:py:class:`~ikomia.core.pycore.CWorkflowTask` or derived): object instance.
-        index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-    Returns:
-        :py:class:`~ikomia.dataprocess.pydataprocess.CGraphicsOutput`: output if index is set, list of outputs otherwise.
-    """
-    data_types = [core.IODataType.OUTPUT_GRAPHICS]
-    return _get_outputs(task, data_types, index=index)
-
-
-def get_numeric_output(task, index=-1):
-    """
-    Get NUMERIC output(s) of the given task. A task can have multiple NUMERIC outputs so the index
-    argument can be set to specify the wanted output.
-
-    Args:
-        task (:py:class:`~ikomia.core.pycore.CWorkflowTask` or derived): object instance.
-        index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-    Returns:
-        :py:class:`~ikomia.dataprocess.pydataprocess.CNumericIO`: output if index is set, list of outputs otherwise.
-    """
-    data_types = [core.IODataType.NUMERIC_VALUES]
-    return _get_outputs(task, data_types, target_class=dataprocess.CNumericIO, index=index)
-
-
-def get_data_string_output(task, index=-1):
-    """
-    Get data string output(s) of the given task. A task can have multiple NUMERIC outputs so the index
-    argument can be set to specify the wanted output.
-
-    Args:
-        task (:py:class:`~ikomia.core.pycore.CWorkflowTask` or derived): object instance.
-        index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-    Returns:
-        :py:class:`~ikomia.dataprocess.pydataprocess.CDataStringIO`: output if index is set, list of outputs otherwise.
-    """
-    data_types = [core.IODataType.NUMERIC_VALUES]
-    return _get_outputs(task, data_types, target_class=dataprocess.CDataStringIO, index=index)
-
-
-def get_blob_measure_output(task, index=-1):
-    """
-    Get BLOB MEASURE output(s) of the given task. A task can have multiple BLOB MEASURE outputs so the index
-    argument can be set to specify the wanted output.
-
-    Args:
-        task (:py:class:`~ikomia.core.pycore.CWorkflowTask` or derived): object instance.
-        index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-    Returns:
-        :py:class:`~ikomia.dataprocess.pydataprocess.CBlobMeasureIO`: output if index is set, list of outputs otherwise.
-    """
-    data_types = [core.IODataType.BLOB_VALUES]
-    return _get_outputs(task, data_types, index=index)
-
-
-def get_dataset_output(task, index=-1):
-    """
-    Get DATASET output(s) of the given task. A task can have multiple DATASET outputs so the index
-    argument can be set to specify the wanted output.
-
-    Args:
-        task (:py:class:`~ikomia.core.pycore.CWorkflowTask` or derived): object instance.
-        index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-    Returns:
-        :py:class:`~ikomia.dataprocess.pydataprocess.CDatasetIO`: output if index is set, list of outputs otherwise.
-    """
-    data_types = [core.IODataType.DNN_DATASET]
-    return _get_outputs(task, data_types, index=index)
-
-
-def get_array_output(task, index=-1):
-    """
-    Get ARRAY output(s) of the given task. A task can have multiple ARRAY outputs so the index
-    argument can be set to specify the wanted output.
-
-    Args:
-        task (:py:class:`~ikomia.core.pycore.CWorkflowTask` or derived): object instance.
-        index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-    Returns:
-        :py:class:`~ikomia.dataprocess.pydataprocess.CArrayIO`: output if index is set, list of outputs otherwise.
-    """
-    data_types = [core.IODataType.ARRAY]
-    return _get_outputs(task, data_types, index=index)
-
-
-def get_path_output(task, index=-1):
-    """
-    Get ARRAY output(s) of the given task. A task can have multiple ARRAY outputs so the index
-    argument can be set to specify the wanted output.
-
-    Args:
-        task (:py:class:`~ikomia.core.pycore.CWorkflowTask` or derived): object instance.
-        index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-    Returns:
-        :py:class:`~ikomia.dataprocess.pydataprocess.CPathIO`: output if index is set, list of outputs otherwise.
-    """
-    data_types = [core.IODataType.FOLDER_PATH, core.IODataType.FILE_PATH]
-    return _get_outputs(task, data_types, index=index)
-
-
-def get_object_detection_output(task, index=-1):
-    """
-    Get Object Detection output(s) of the given task. A task can have multiple Object Detection outputs so the index
-    argument can be set to specify the wanted output.
-
-    Args:
-        task (:py:class:`~ikomia.core.pycore.CWorkflowTask` or derived): object instance.
-        index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-    Returns:
-        :py:class:`~ikomia.dataprocess.pydataprocess.CObjectDetectionIO`: output if index is set, list of outputs otherwise.
-    """
-    data_types = [core.IODataType.OBJECT_DETECTION]
-    return _get_outputs(task, data_types, index=index)
-
-
-def get_instance_segmentation_output(task, index=-1):
-    """
-    Get Instance Segmentation output(s) of the given task. A task can have multiple Instance Segmentation outputs so the
-    index argument can be set to specify the wanted output.
-
-    Args:
-        task (:py:class:`~ikomia.core.pycore.CWorkflowTask` or derived): object instance.
-        index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-    Returns:
-        :py:class:`~ikomia.dataprocess.pydataprocess.CInstanceSegIO`: output if index is set, list of outputs otherwise.
-    """
-    data_types = [core.IODataType.INSTANCE_SEGMENTATION]
-    return _get_outputs(task, data_types, index=index)
-
-
-def get_semantic_segmentation_output(task, index=-1):
-    """
-    Get Semantic Segmentation output(s) of the given task. A task can have multiple Semantic Segmentation outputs so the
-    index argument can be set to specify the wanted output.
-
-    Args:
-        task (:py:class:`~ikomia.core.pycore.CWorkflowTask` or derived): object instance.
-        index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-    Returns:
-        :py:class:`~ikomia.dataprocess.pydataprocess.CSemanticSegIO`: output if index is set, list of outputs otherwise.
-    """
-    data_types = [core.IODataType.SEMANTIC_SEGMENTATION]
-    return _get_outputs(task, data_types, index=index)
+        raise RuntimeError(f"No output at index {index}: only {len(outputs)} outputs available")
diff --git a/ikomia/dataprocess/datadictIO.py b/ikomia/dataprocess/datadictIO.py
index 5263e330..a49fd9f9 100644
--- a/ikomia/dataprocess/datadictIO.py
+++ b/ikomia/dataprocess/datadictIO.py
@@ -33,13 +33,13 @@ def __init__(self):
         CWorkflowTaskIO.__init__(self, IODataType.DATA_DICT)
         self.data = {}
 
-    def clearData(self):
+    def clear_data(self):
         """
         Clear whole data dict.
         """
         self.data.clear()
 
-    def isDataAvailable(self):
+    def is_data_available(self):
         """
         Check whether the dataset structure contains data.
 
@@ -68,7 +68,7 @@ def load(self, path):
         with open(path, "r") as infile:
             self.data = json.load(infile)
 
-    def toJson(self, options=[]):
+    def to_json(self, options=[]):
         json_format = "compact"
         if "json_format" in options:
             json_format = options[options.index("json_format") + 1]
@@ -78,5 +78,5 @@ def toJson(self, options=[]):
         else:
             return json.dumps(self.data)
 
-    def fromJson(self, json_string):
+    def from_json(self, json_string):
         self.data = json.loads(json_string)
diff --git a/ikomia/dataprocess/registry.py b/ikomia/dataprocess/registry.py
index a780e242..7ad84d61 100644
--- a/ikomia/dataprocess/registry.py
+++ b/ikomia/dataprocess/registry.py
@@ -36,13 +36,15 @@ class IkomiaRegistry(dataprocess.CIkomiaRegistry):
     install, update and instanciate any of these algorithms.
     Derived from :py:class:`~ikomia.dataprocess.pydataprocess.CIkomiaRegistry`.
     """
-    def __init__(self, lazy_load:bool = True ):
+    def __init__(self, lazy_load:bool = True):
         dataprocess.CIkomiaRegistry.__init__(self)
 
         if not lazy_load:
             self.load_algorithms()
 
-    @utils.http.http_except
+    def __repr__(self):
+        return f"IkomiaRegistry()"
+
     def get_online_algorithms(self):
         """
         Get the list of available algorithms from Ikomia HUB.
@@ -54,8 +56,8 @@ def get_online_algorithms(self):
              list of dict: list of algorithms information
         """
         s = ikomia.ik_api_session
-        if s is None or s.token is None:
-            raise ConnectionError("Failed to get online algorithms from Ikomia HUB, authentication required.")
+        if s is None:
+            raise ConnectionError("Failed to get online algorithms from Ikomia HUB.")
 
         url = config.main_cfg["hub"]["url"] + "/api/plugin/"
         r = s.session.get(url)
@@ -69,7 +71,7 @@ def get_online_algorithms(self):
 
         return platform_plugins
 
-    def create_algorithm(self, name, parameters=None):
+    def create_algorithm(self, name:str, parameters=None, no_hub:bool=False):
         """
         Instanciate algorithm from its unique name. See :py:meth:`~ikomia.dataprocess.IkomiaRegistry.get_algorithms` or
         :py:meth:`~ikomia.dataprocess.IkomiaRegistry.get_online_algorithms` to get valid names.
@@ -80,6 +82,7 @@ def create_algorithm(self, name, parameters=None):
         Args:
             name (str): unique algorithm name
             parameters (~ikomia.core.CWorkflowTaskParam): initial parameters values
+            no_hub (bool): if True, we don't try to install algorithm from Ikomia Hub
 
         Returns:
             :py:class:`~ikomia.core.pycore.CWorkflowTask` or derived: algorithm instance
@@ -91,15 +94,21 @@ def create_algorithm(self, name, parameters=None):
             algo = self.create_instance(name, parameters)
         else:
             try:
-                self._load_algorithm(name)
+                algo_dir, language = self._get_algorithm_directory(name)
+                self._load_algorithm(name, algo_dir, language)
                 algo = self.create_instance(name, parameters)
-            except:
-                try:
-                    logger.warning(f"Try installing {name} from Ikomia HUB...")
-                    self.install_algorithm(name)
-                    algo = self.create_instance(name, parameters)
-                except Exception as e:
-                    logger.error(e)
+            except Exception as e:
+                logger.warning(e)
+
+                # If algorithm is installed but not functional (algo_dir is not empty), it may be a plugin
+                # in developpement and we should not overwrite it with the Ikomia Hub version
+                if not no_hub and not algo_dir:
+                    try:
+                        logger.warning(f"Try installing {name} from Ikomia HUB...")
+                        self.install_algorithm(name)
+                        algo = self.create_instance(name, parameters)
+                    except Exception as e:
+                        logger.error(e)
 
         return algo
 
@@ -114,7 +123,7 @@ def update_algorithms(self):
             if not info.internal:
                 self.update_algorithm(algo)
 
-    def update_algorithm(self, name):
+    def update_algorithm(self, name:str):
         """
         Launch update of the given algorithm. It only concerns algorithms of Ikomia HUB.
         The function checks version compatibility.
@@ -152,7 +161,7 @@ def update_algorithm(self, name):
         else:
             self.install_algorithm(name, force=True)
 
-    def install_algorithm(self, name, force=False):
+    def install_algorithm(self, name:str, force:bool=False):
         """
         Launch algorithm installation from Ikomia HUB given its unique name.
 
@@ -171,44 +180,51 @@ def install_algorithm(self, name, force=False):
                 update = True
 
         # Download package
-        plugin, language, plugin_dir = self._download_algorithm(name)
+        try:
+            plugin, language, algo_dir = self._download_algorithm(name)
+        except Exception as e:
+            logger.error(f"Failed to install algorithm {name} for the following reason:")
+            logger.error(e)
+            return
 
         # Install requirements
         logger.info(f"Installing {name} requirements. This may take a while, please be patient...")
-        utils.plugintools.install_requirements(plugin_dir)
-        self._check_installed_modules(plugin_dir)
+        utils.plugintools.install_requirements(algo_dir)
+        self._check_installed_modules(algo_dir)
 
         # Load it
-        self._load_algorithm(name, language)
+        self._load_algorithm(name, algo_dir, language)
         if language == utils.ApiLanguage.CPP and update:
             logger.warning(f"C++ algorithm {plugin['name']} can't be reloaded at runtime. "
                            f"It will be updated on next start.")
 
-    def _load_algorithm(self, name, language=None):
-        if language is None:
-            # C++ or Python algorithm?
-            cpp_algo_dir = os.path.join(self.get_plugins_directory(), "C++", name)
-            python_algo_dir = os.path.join(self.get_plugins_directory(), "Python", name)
+        if config.main_cfg["registry"]["auto_completion"]:
+            autocomplete.update_local_plugin(name)
 
-            if os.path.isdir(cpp_algo_dir):
-                self.load_cpp_algorithm(cpp_algo_dir)
-            elif os.path.isdir(python_algo_dir):
-                self.load_python_algorithm(python_algo_dir)
-            else:
-                raise RuntimeError(f"Algorithm {name} is not installed.")
-        elif language == utils.ApiLanguage.PYTHON:
-            python_algo_dir = os.path.join(self.get_plugins_directory(), "Python", name)
-            self.load_python_algorithm(python_algo_dir)
+    def _get_algorithm_directory(self, name:str):
+        # C++ or Python algorithm?
+        cpp_algo_dir = os.path.join(self.get_plugins_directory(), "C++", name)
+        python_algo_dir = os.path.join(self.get_plugins_directory(), "Python", name)
+
+        if os.path.isdir(cpp_algo_dir):
+            return cpp_algo_dir, utils.ApiLanguage.CPP
+        elif os.path.isdir(python_algo_dir):
+            return python_algo_dir, utils.ApiLanguage.PYTHON
+        else:
+            return "", None
+
+    def _load_algorithm(self, name:str, directory:str, language:utils.ApiLanguage):
+        if not os.path.isdir(directory):
+            raise RuntimeError(f"Algorithm {name} is not installed.")
+
+        if language == utils.ApiLanguage.PYTHON:
+            self.load_python_algorithm(directory)
         elif language == utils.ApiLanguage.CPP:
-            cpp_algo_dir = os.path.join(self.get_plugins_directory(), "C++", name)
-            self.load_cpp_algorithm(plugin_dir)
+            self.load_cpp_algorithm(directory)
         else:
             raise RuntimeError(f"Unsupported language for algorithm {name}.")
 
-        if config.main_cfg["registry"]["auto_completion"]:
-            autocomplete.update_local_plugin(name)
-
-    def _download_algorithm(self, name):
+    def _download_algorithm(self, name:str):
         available_plugins = self.get_online_algorithms()
 
         plugin_info = None
@@ -227,15 +243,11 @@ def _download_algorithm(self, name):
         if state != utils.PluginState.VALID:
             error_msg = f"Plugin {plugin['name']} can't be installed due to version incompatibility.\n"\
                         f"Based on Ikomia {plugin['ikomiaVersion']} " \
-                        f"while the current version is {utils.getApiVersion()}."
+                        f"while the current version is {utils.get_api_version()}."
             raise ValueError(error_msg)
 
-        s = ikomia.ik_api_session
-        if s.token is None:
-            error_msg = "Online algorithms retrieval failed, authentication required."
-            raise ConnectionRefusedError(error_msg)
-
         # Get plugin package url
+        s = ikomia.ik_api_session
         url = config.main_cfg["hub"]["url"] + "/api/plugin/" + str(plugin_info["id"]) + "/package/"
         r = s.session.get(url)
         r.raise_for_status()
@@ -245,7 +257,7 @@ def _download_algorithm(self, name):
         # Download package
         url = config.main_cfg["hub"]["url"] + package_url
         file_path = os.path.join(self.get_plugins_directory(), "Transfer", os.path.basename(package_url))
-        utils.http.download_file(url, file_path, public=False)
+        utils.http.download_file(url, file_path, public=True)
 
         # Unzip
         language_folder = "C++" if language == utils.ApiLanguage.CPP else "Python"
@@ -283,7 +295,7 @@ def _check_compatibility(plugin):
 
         return True
 
-    def _check_installed_modules(self, plugin_dir):
+    def _check_installed_modules(self, algo_dir:str):
         modules = utils.plugintools.get_installed_modules()
 
         # Uninstall blacklisted packages (conflicting with already bundle packages in Ikomia API)
@@ -302,7 +314,7 @@ def _check_installed_modules(self, plugin_dir):
                     utils.plugintools.uninstall_package(package)
 
         # Remove plugin specific blacklisted packages
-        needless_path = os.path.join(plugin_dir, "needless.txt")
+        needless_path = os.path.join(algo_dir, "needless.txt")
         if os.path.exists(needless_path):
             with open(needless_path, "r") as f:
                 for line in f:
diff --git a/ikomia/dataprocess/workflow.py b/ikomia/dataprocess/workflow.py
index 59c78828..b8cb3350 100644
--- a/ikomia/dataprocess/workflow.py
+++ b/ikomia/dataprocess/workflow.py
@@ -20,10 +20,10 @@
 import os
 import logging
 import enum
-
+import datetime
 import ikomia
 from ikomia import utils, core, dataprocess, dataio
-from ikomia.core import config, task
+from ikomia.core import config, task, IODataType
 from urllib.parse import urlparse
 
 logger = logging.getLogger(__name__)
@@ -42,7 +42,7 @@ class RunMode(enum.Enum):
         SINGLE = 1
         DIRECTORY = 2
 
-    def __init__(self, name="Untitled", registry=None):
+    def __init__(self, name="Untitled", registry=ikomia.ik_registry):
         """
         Construct Workflow object with the given name and an :py:class:`~ikomia.dataprocess.registry.IkomiaRegistry`
         object. The latter is used to instanciate algorithm from their unique name when added to the workflow. Thus,
@@ -55,6 +55,18 @@ def __init__(self, name="Untitled", registry=None):
 
         self.registry = registry
         self.output_folder = os.path.join(config.main_cfg["workflow"]["path"], self.name) + os.sep
+        root_id = self.get_root_id()
+        self.root_uuid = self.get_task(root_id).uuid
+        self.task_to_id = {self.root_uuid: root_id}
+
+    def __repr__(self):
+        return f"Workflow(\"{self.name}\")"
+
+    def root(self):
+        """
+        Get workflow root node.
+        """
+        return self.get_task(self.get_root_id())
 
     def set_image_input(self, array=None, path="", url="", index=-1, datatype=core.IODataType.IMAGE):
         """
@@ -121,12 +133,12 @@ def set_directory_input(self, folder="", index=-1):
             return
 
         dir_input = dataprocess.CPathIO(core.IODataType.FOLDER_PATH, folder)
-        if index == -1:
+        if index == -1 or index >= self.get_input_count():
             self.add_input(dir_input)
         else:
             self.set_input(dir_input, index, True)
 
-    def set_parameters(self, params: dict, task_id=None, task_name="", index=-1):
+    def set_parameters(self, params: dict, task_obj=None, task_name="", index=-1):
         """
         Set task parameters as a simple key-value dict.
         You can get parameters keys for each by calling:
@@ -137,27 +149,28 @@ def set_parameters(self, params: dict, task_id=None, task_name="", index=-1):
 
         Args:
             params (dict): key-value pairs of parameters to modify.
-            task_id (int): unique identifier of the task. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
-            task_name (str): method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
+            task_obj (:py:class:`~ikomia.core.CWorkflowTask` based object): task instance. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
+            task_name (str): algorithm name to be found. Multiple candidates may exist, so use task_index parameter to specify one. Method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
             index (int): zero-based index of the wanted task. If -1, the function modifies all candidates parameters.
         """
-        if task_id is not None:
-            wf_task = self.get_task(task_id)
-            if wf_task is not None:
-                wf_task.set_parameters(params)
+        if task_obj is None and not task_name:
+            raise RuntimeError("Unable to set task parameters: parameters must include either a valid name or task instance.")
+
+        if task_obj is not None:
+            task_obj.set_parameters(params)
         elif task_name:
-            wf_task = self.find_task(task_name)
-            if wf_task is None:
-                return
+            task_obj = self.find_task(task_name)
+            if task_obj is None:
+                raise RuntimeError(f"Algorithm {task_name} can't be found.")
 
-            if not isinstance(wf_task, list):
-                wf_task[1].set_parameters(params)
+            if not isinstance(task_obj, list):
+                task_obj.set_parameters(params)
             else:
                 if index == -1:
-                    for t in wf_task:
-                        t[1].set_parameters(params)
+                    for t in task_obj:
+                        t.set_parameters(params)
                 elif 0 <= index < len(wf_task):
-                    wf_task[index][1].set_parameters(params)
+                    task_obj[index].set_parameters(params)
 
     def get_time_metrics(self):
         """
@@ -177,296 +190,140 @@ def get_time_metrics(self):
 
         return metrics
 
-    def get_image_output(self, task_id=None, task_name="", index=-1):
-        """
-        Get workflow IMAGE output(s) for the given task(s). You can either pass a unique task ID or a task name.
-        In the latter case, several tasks of the workflow can match the name. The function will then return all
-        corresponding outputs in a list. Moreover, a task can have multiple IMAGE outputs so the index (zero-based)
-        argument can be set to specify the wanted output.
-
-        Args:
-            task_id (int): unique identifier of the task. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
-            task_name (str): method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
-            index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-        Returns:
-            :py:class:`~ikomia.dataprocess.pydataprocess.CImageIO`: output or list of outputs.
-        """
-        return self._get_task_output(task.get_image_output, task_id, task_name, index)
-
-    def get_image(self, task_id=None, task_name="", index=-1):
+    def get_task_output(self, task_obj=None, task_name="", task_index=0, types=[IODataType.IMAGE], output_index=-1):
         """
-        Get image (numpy array) from a specific IMAGE output (by index) of the given task (by name or id).
-        If task_name argument is given, the function returns images from all matching tasks. If index = -1,
-        the function returns also images from all IMAGE outputs available.
+        Get specific output(s) defined by their types (:py:class:`~ikomia.core.PyCore.IODataType`) for the given task.
 
         Args:
-            task_id (int): unique identifier of the task. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
-            task_name (str): method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
-            index (int): zero-based index of the IMAGE output.
+            task_obj (task_obj (:py:class:`~ikomia.core.CWorkflowTask` based object): task instance. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
+            task_name (str): algorithm name to be found. Multiple candidates may exist, so use task_index parameter to specify one. Method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
+            task_index (int): zero-based index of the wanted task. If -1, the function returns all candidates outputs.
+            types (list of :py:class:`~ikomia.core.PyCore.IODataType`): output data types.
+            output_index (int): zero-based index of he wanted output.
 
         Returns:
-            Numpy array or list: result image(s).
-        """
-        outputs = self.get_image_output(task_id=task_id, task_name=task_name, index=index)
-        if isinstance(outputs, list):
-            images = []
-            for output in outputs:
-                images.append(output.get_tmage())
-            return images
-        else:
-            return outputs.get_image()
-
-    def get_image_with_graphics(self, task_id=None, task_name="", image_index=0, graphics_index=0):
+            :py:class:`~ikomia.dataprocess.pydataprocess.CWorkflowTaskIO` based object: task output of the given data types (can be a list).
         """
-        Get image (numpy array) from a specific IMAGE output of the given task with graphics items burnt into it.
-        Graphics items came from a specific GRAPHICS output of the same task. If task_name argument is given, the
-        function returns images from all matching tasks.
+        if task_obj is None and not task_name:
+            raise RuntimeError("Unable to get task output: parameters must include either a valid name or task instance.")
 
-        Args:
-            task_id (int): unique identifier of the task. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
-            task_name (str): method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
-            image_index (int): zero-based index of the IMAGE output.
-            graphics_index (int): zero-based index of the GRAPHICS output.
+        if task_obj is None:
+            task_obj = self.find_task(task_name, task_index)
+            if task_obj is None:
+                raise RuntimeError(f"Algorithm {task_name} can't be found.")
 
-        Returns:
-            Numpy array or list: result image(s).
-        """
-        if task_id is not None:
-            obj = self.getTask(task_id)
-            return task.get_image_with_graphics(obj, image_index, graphics_index)
-        elif task_name:
-            wf_task = self.find_task(task_name)
-            if not isinstance(wf_task, list):
-                return task.get_image_with_graphics(wf_task[1], image_index, graphics_index)
+        outputs = []
+        if isinstance(task_obj, list):
+            if len(task_obj) == 1:
+                return task.get_output(task_obj[0], types, output_index)
             else:
-                images = []
-                for t in wf_task:
-                    images.append(task.get_image_with_graphics(t[1], image_index, graphics_index))
-
-                return images
-
-    def get_graphics_output(self, task_id=None, task_name="", index=-1):
-        """
-        Get workflow GRAPHICS output(s) for the given task(s). You can either pass a unique task ID or a task name.
-        In the latter case, several tasks of the workflow can match the name. The function will then return all
-        corresponding outputs in a list. Moreover, a task can have multiple GRAPHICS outputs so the index (zero-based)
-        argument can be set to specify the wanted output.
-
-        Args:
-            task_id (int): unique identifier of the task. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
-            task_name (str): method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
-            index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-        Returns:
-            :py:class:`~ikomia.dataprocess.pydataprocess.CGraphicsOutput`: output or list of outputs.
-        """
-        return self._get_task_output(task.get_graphics_output, task_id, task_name, index)
-
-    def get_numeric_output(self, task_id=None, task_name="", index=-1):
-        """
-        Get workflow NUMERIC output(s) for the given task(s). You can either pass a unique task ID or a task name.
-        In the latter case, several tasks of the workflow can match the name. The function will then return all
-        corresponding outputs in a list. Moreover, a task can have multiple NUMERIC outputs so the index (zero-based)
-        argument can be set to specify the wanted output.
-
-        Args:
-            task_id (int): unique identifier of the task. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
-            task_name (str): method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
-            index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-        Returns:
-            :py:class:`~ikomia.dataprocess.pydataprocess.CNumericIO`: output or list of outputs.
-        """
-        return self._get_task_output(task.get_numeric_output, task_id, task_name, index)
-
-    def get_data_string_output(self, task_id=None, task_name="", index=-1):
-        """
-        Get workflow data string output(s) for the given task(s). You can either pass a unique task ID or a task name.
-        In the latter case, several tasks of the workflow can match the name. The function will then return all
-        corresponding outputs in a list. Moreover, a task can have multiple string outputs so the index (zero-based)
-        argument can be set to specify the wanted output.
-
-        Args:
-            task_id (int): unique identifier of the task. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
-            task_name (str): method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
-            index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-        Returns:
-            :py:class:`~ikomia.dataprocess.pydataprocess.CDataStringIO`: output or list of outputs.
-        """
-        return self._get_task_output(task.get_data_string_output, task_id, task_name, index)
-
-    def get_blob_measure_output(self, task_id=None, task_name="", index=-1):
-        """
-        Get workflow BLOB MEASURE output(s) for the given task(s). You can either pass a unique task ID or a task name.
-        In the latter case, several tasks of the workflow can match the name. The function will then return all
-        corresponding outputs in a list. Moreover, a task can have multiple BLOB MEASURE outputs so the index (zero-based)
-        argument can be set to specify the wanted output.
-
-        Args:
-            task_id (int): unique identifier of the task. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
-            task_name (str): method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
-            index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
-
-        Returns:
-            :py:class:`~ikomia.dataprocess.pydataprocess.CBlobMeasureIO`: output or list of outputs.
-        """
-        return self._get_task_output(task.get_blob_measure_output, task_id, task_name, index)
-
-    def get_dataset_output(self, task_id=None, task_name="", index=-1):
-        """
-        Get workflow DATASET output(s) for the given task(s). You can either pass a unique task ID or a task name.
-        In the latter case, several tasks of the workflow can match the name. The function will then return all
-        corresponding outputs in a list. Moreover, a task can have multiple DATASET outputs so the index (zero-based)
-        argument can be set to specify the wanted output.
-
-        Args:
-            task_id (int): unique identifier of the task. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
-            task_name (str): method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
-            index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
+                for t in task_obj:
+                    outs = task.get_output(t, types, output_index)
+                    if isinstance(outs, list):
+                        outputs.extend(outs)
+                    else:
+                        outputs.append(outs)
 
-        Returns:
-            :py:class:`~ikomia.dataprocess.pydataprocess.CDatasetIO`: output or list of outputs.
-        """
-        return self._get_task_output(task.get_dataset_output, task_id, task_name, index)
+                return outputs
+        else:
+            return task.get_output(task_obj, types, output_index)
 
-    def get_array_output(self, task_id=None, task_name="", index=-1):
+    def get_tasks(self):
         """
-        Get workflow ARRAY output(s) for the given task(s). You can either pass a unique task ID or a task name.
-        In the latter case, several tasks of the workflow can match the name. The function will then return all
-        corresponding outputs in a list. Moreover, a task can have multiple ARRAY outputs so the index (zero-based)
-        argument can be set to specify the wanted output.
-
-        Args:
-            task_id (int): unique identifier of the task. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
-            task_name (str): method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
-            index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
+        Get all tasks composing the workflow.
 
         Returns:
-            :py:class:`~ikomia.dataprocess.pydataprocess.CArrayIO`: output or list of outputs.
-        """
-        return self._get_task_output(task.get_array_output, task_id, task_name, index)
-
-    def get_path_output(self, task_id=None, task_name="", index=-1):
+             list of :py:class:`~ikomia.core.pycore.CWorkflowTask` based object
         """
-        Get workflow PATH output(s) for the given task(s). You can either pass a unique task ID or a task name.
-        In the latter case, several tasks of the workflow can match the name. The function will then return all
-        corresponding outputs in a list. Moreover, a task can have multiple PATH outputs so the index (zero-based)
-        argument can be set to specify the wanted output.
-
-        Args:
-            task_id (int): unique identifier of the task. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
-            task_name (str): method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
-            index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
+        tasks = []
+        ids = self.get_task_ids()
+        for task_id in ids:
+            t = self.get_task(task_id)
+            if t is not None:
+                tasks.append(t)
 
-        Returns:
-            :py:class:`~ikomia.dataprocess.pydataprocess.CPathIO`: output or list of outputs.
-        """
-        return self._get_task_output(task.get_path_output, task_id, task_name, index)
+        return tasks
 
-    def get_object_detection_output(self, task_id=None, task_name="", index=-1):
+    def get_task_id(self, task):
         """
-        Get workflow Object Detection output(s) for the given task(s). You can either pass a unique task ID or a task
-        name. In the latter case, several tasks of the workflow can match the name. The function will then return all
-        corresponding outputs in a list. Moreover, a task can have multiple Object Detection outputs so the
-        index (zero-based) argument can be set to specify the wanted output.
+        Get task unique identifier from the task instance/
 
         Args:
-            task_id (int): unique identifier of the task. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
-            task_name (str): method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
-            index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
+            task (:py:class:`~ikomia.core.pycore.CWorkflowTask` based object): task instance
 
         Returns:
-            :py:class:`~ikomia.dataprocess.pydataprocess.CObjectDetectionIO`: output or list of outputs.
+            int: task unique identifier
         """
-        return self._get_task_output(task.get_object_detection_output, task_id, task_name, index)
+        return self.task_to_id[task.uuid]
 
-    def get_instance_segmentation_output(self, task_id=None, task_name="", index=-1):
+    def add_task(self, task=None, name:str="", params:dict={}, auto_connect:bool=False):
         """
-        Get workflow Instance Segmentation output(s) for the given task(s). You can either pass a unique task ID or a
-        task name. In the latter case, several tasks of the workflow can match the name. The function will then return
-        all corresponding outputs in a list. Moreover, a task can have multiple Instance Segmentation outputs so the
-        index (zero-based) argument can be set to specify the wanted output.
+        Add task identified by its unique name in the workflow. If the given task is not yet in the registry, it will be
+        firstly downloaded and installed from Ikomia HUB. Task unique identifier can then be retrieved with
+        :py:meth:`get_task_id`.
 
         Args:
-            task_id (int): unique identifier of the task. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
-            task_name (str): method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
-            index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
+            name (str): algorithm unique name
+            param (:py:class:`~ikomia.core.pycore.CWorkflowTaskParam` based object): algorithm parameters
 
         Returns:
-            :py:class:`~ikomia.dataprocess.pydataprocess.CInstanceSegIO`: output or list of outputs.
-        """
-        return self._get_task_output(task.get_instance_segmentation_output, task_id, task_name, index)
-
-    def get_semantic_segmentation_output(self, task_id=None, task_name="", index=-1):
+            :py:class:`~ikomia.core.pycore.CWorkflowTask` based object: task instance
         """
-        Get workflow Semantic Segmentation output(s) for the given task(s). You can either pass a unique task ID or a
-        task name. In the latter case, several tasks of the workflow can match the name. The function will then return
-        all corresponding outputs in a list. Moreover, a task can have multiple Semantic Segmentation outputs so the
-        index (zero-based) argument can be set to specify the wanted output.
+        if task is None and not name:
+            raise RuntimeError("Unable to add task to workflow: parameters must include either a valid name or task instance.")
 
-        Args:
-            task_id (int): unique identifier of the task. See also :py:meth:`~ikomia.dataprocess.workflow.add_task` and :py:meth:`~ikomia.dataprocess.workflow.find_task`.
-            task_name (str): method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
-            index (int): zero-based index of the output in case of multiple matches. With default -1, all outputs are returned.
+        if task is None:
+            task = self.registry.create_algorithm(name, None)
+            if task is None:
+                raise RuntimeError(f"Algorithm {name} can't be created.")
 
-        Returns:
-            :py:class:`~ikomia.dataprocess.pydataprocess.CSemanticSegIO`: output or list of outputs.
-        """
-        return self._get_task_output(task.get_semantic_segmentation_output, task_id, task_name, index)
+        if len(params) > 0:
+            task.set_parameters(params)
 
-    def get_tasks(self):
-        """
-        Get unique identifier and name for all tasks composing the workflow.
+        parent_id = self.get_last_task_id()
+        task_id = super().add_task(task)
+        self.task_to_id[task.uuid] = task_id
 
-        Returns:
-             list of tuple (id (int), name (str)
-        """
-        tasks = []
-        ids = self.get_task_ids()
-        for task_id in ids:
-            t = self.get_task(task_id)
-            if t is not None:
-                tasks.append((task_id, t.name))
+        if (auto_connect):
+            parent_task = self.get_task(parent_id)
+            if parent_task is not None:
+                self.connect_tasks(parent_task, task)
+            else:
+                self.connect_tasks(self.root(), task)
 
-        return tasks
+        return task
 
-    def add_task(self, task=None, name:str="", params:dict={}):
+    def remove_task(self, task=None, name: str="", index: int=0):
         """
-        Add task identified by its unique name in the workflow. If the given task is not yet in the registry, it will be
-        firstly downloaded and installed from Ikomia HUB.
+        Remove task from workflow specified by task instance or name (with corresponding index).
 
         Args:
-            name (str): algorithm unique name
-            param (:py:class:`~ikomia.core.pycore.CWorkflowTaskParam` or derived): algorithm parameters
-
-        Returns:
-            pair (tuple): 1- unique task identifier 2- task object
+            task (:py:class:`~ikomia.core.pycore.CWorkflowTask`): task object instance
+            name (str): algorithm name to be found. Multiple candidates may exist, so use task_index parameter to specify one. Method :py:meth:`~ikomia.dataprocess.workflow.find_task` is used to retrieve corresponding task(s).
+            index (int): zero-based index of the wanted task.
         """
         if task is None and not name:
-            raise RuntimeError("Unable to add task to workflow: parameters must include either a valid name or task instance.")
+            raise RuntimeError("Unable to remove task: parameters must include either a valid name or task instance.")
 
         if task is None:
-            task = self.registry.create_algorithm(name, None)
+            task = self.find_task(name, index)
             if task is None:
-                raise RuntimeError(f"Algorithm {name} can't be created.")
-
-        if len(params) > 0:
-            task.set_parameters(params)
+                raise RuntimeError(f"Algorithm {name} can't be found.")
 
-        return super().add_task(task), task
+        super().delete_task(self.task_to_id[task.uuid])
+        del self.task_to_id[task.uuid]
 
     def find_task(self, name: str, index=-1):
         """
         Get identifiers and instance of tasks with the given name in the workflow.
 
         Args:
-             name (str): algorithm name
+             name (str): algorithm name. Multiple candidates may exist, so use index parameter to specify one.
              index (int): zero-based index of the wanted task. If -1, the function returns all candidates.
 
         Returns:
-            list of pairs (tuple): 1- task identifier 2- task object instance
+            list of :py:class:`~ikomia.core.pycore.CWorkflowTask` based objects: if index == -1
+            :py:class:`~ikomia.core.pycore.CWorkflowTask` based object : if index != -1
         """
         tasks = []
         ids = self.get_task_ids()
@@ -474,12 +331,10 @@ def find_task(self, name: str, index=-1):
         for task_id in ids:
             task = self.get_task(task_id)
             if task.name == name:
-                tasks.append((task_id, task))
+                tasks.append(task)
 
         if 0 <= index < len(tasks):
             return tasks[index]
-        elif len(tasks) == 1:
-            return tasks[0]
         else:
             return tasks
 
@@ -490,15 +345,22 @@ def connect_tasks(self, src, target, edges=None):
         and the input index of the target task.
 
         Args:
-            src (int): source task identifier
-            target (int): target task identifier
+            src (:py:class:`~ikomia.core.pycore.CWorkflowTask` based object): source task or None (connect to root)
+            target (:py:class:`~ikomia.core.pycore.CWorkflowTask` based object): target task
             edges (list of pair): connections. If *None* is passed, auto-connection is enabled so that the system will try to find the best connections automatically with respect to inputs and outputs data types.
         """
+        if src.uuid == self.root_uuid:
+            src_id = self.get_root_id()
+        else:
+            src_id = self.get_task_id(src)
+
+        target_id = self.get_task_id(target)
+
         if edges is None:
-            self.connect(src, target, -1, -1)
+            self.connect(src_id, target_id, -1, -1)
         else:
             for edge in edges:
-                self.connect(src, target, edge[0], edge[1])
+                self.connect(src_id, target_id, edge[0], edge[1])
 
     def run(self):
         """
@@ -514,8 +376,16 @@ def run(self):
 
         if run_mode == Workflow.RunMode.SINGLE:
             super().run()
+            metrics = self.get_time_metrics()
+            total_time = metrics['total_time']
         else:
+            time_start = datetime.datetime.now()
             self._run_directory()
+            time_stop = datetime.datetime.now()
+            total_time = (time_stop - time_start).total_seconds() * 1000
+            logger.info(f"Workflow output data are saved in {self.get_last_run_folder()}")
+
+        logger.info(f"Workflow {self.name} run successfully in {total_time} ms.")
 
     def run_on(self, array=None, path="", url="", folder=""):
         """
@@ -534,9 +404,8 @@ def run_on(self, array=None, path="", url="", folder=""):
             raise RuntimeError("Workflow input is invalid: you must pass either an numpy array, a path to image/video"
                                "or a folder containing images or videos.")
 
-        self.update_start_time()
-
         if folder:
+            self.set_auto_save(True)
             self.set_directory_input(folder=folder)
             self.run()
         else:
@@ -562,28 +431,35 @@ def prepare_runtime_env(self, path):
 
         for t in tasks:
             if t not in available_tasks:
-                if online_tasks is not None and any(d["name"] == t for d in online_tasks):
-                    # try installation
-                    try:
-                        ikomia.ik_registry.install_algorithm(t)
-                    except Exception:
-                        msg = f"Workflow preparation failed: task {t} cannot be installed."
-                        logger.error(msg)
-                        return False
-                else:
+                try:
+                    ikomia.ik_registry.create_algorithm(t)
+                except Exception as e:
                     msg = f"Workflow preparation failed: task {t} cannot be found."
                     logger.error(msg)
+                    logger.error(e)
                     return False
 
         return True
 
+    def load(self, path):
+        if self.prepare_runtime_env(path):
+            super().load(path)
+
+            # Update map task -> id
+            self.task_to_id.clear()
+            ids = self.get_task_ids()
+
+            for task_id in ids:
+                task = self.get_task(task_id)
+                self.task_to_id[task.uuid] = task_id
+
     def _run_directory(self):
         for i in range(self.get_input_count()):
             input_type = self.get_input_data_type(i)
             if input_type == core.IODataType.FOLDER_PATH:
                 dir_input = self.get_input(i)
 
-                for root, subdirs, files in os.walk(dir_input.getPath(), topdown=True):
+                for root, subdirs, files in os.walk(dir_input.get_path(), topdown=True):
                     for file in files:
                         file_path = os.path.join(root, file)
                         if self._is_image_input(path=file_path):
@@ -616,32 +492,6 @@ def _get_run_mode(self):
         else:
             return Workflow.RunMode.SINGLE
 
-    def _get_task_output(self, get_output_func, task_id=None, task_name="", index=-1):
-        wf_task = None
-        if task_id is not None:
-            wf_task = self.get_task(task_id)
-            if wf_task is None:
-                return None
-
-            return get_output_func(wf_task, index)
-        elif task_name:
-            wf_task = self.find_task(task_name)
-            if wf_task is None:
-                return None
-
-            if not isinstance(wf_task, list):
-                return get_output_func(wf_task[1], index)
-            else:
-                wf_outputs = []
-                for t in wf_task:
-                    outs = get_output_func(t[1], index)
-                    if isinstance(outs, list):
-                        wf_outputs.extend(outs)
-                    else:
-                        wf_outputs.append(outs)
-
-                return wf_outputs
-
     @staticmethod
     def _check_run_input(array, path, url, folder):
         return array is not None or path or url or folder
@@ -696,13 +546,9 @@ def load(path):
     Returns:
         :py:class:`~ikomia.dataprocess.workflow.Workflow`: loaded workflow.
     """
-
     wf = Workflow("untitled", ikomia.ik_registry)
-    if wf.prepare_runtime_env(path):
-        wf.load(path)
-        return wf
-
-    return None
+    wf.load(path)
+    return wf
 
 
 def install_requirements(path):
diff --git a/ikomia/dnn/dataset.py b/ikomia/dnn/dataset.py
index b4d87de6..b3825c04 100644
--- a/ikomia/dnn/dataset.py
+++ b/ikomia/dnn/dataset.py
@@ -23,9 +23,17 @@
 from collections import defaultdict
 import xml.etree.ElementTree as ET
 import numpy as np
-from pycocotools.coco import maskUtils
 import cv2
 import random
+import logging
+
+logger = logging.getLogger(__name__)
+
+try:
+    from pycocotools.coco import maskUtils
+except:
+    logger.info("Pycocotools package is not installed, some dataset loader may not work properly.")
+
 
 _image_extensions = [".jpeg", ".jpg", ".png", ".bmp", ".tiff", ".tif", ".dib", ".jpe", ".jp2", ".webp", ".pbm", ".pgm",
                      ".ppm", ".pxm", ".pnm", ".sr", ".ras", ".exr", ".hdr", ".pic"]
@@ -507,4 +515,4 @@ def polygon_to_mask(polygons, width, height):
         graphics.append(graphics_poly)
 
     conversion = core.CGraphicsConversion(width, height)
-    return conversion.graphicsToBinaryMask(graphics)
+    return conversion.graphics_to_binary_mask(graphics)
diff --git a/ikomia/dnn/dnntrain.py b/ikomia/dnn/dnntrain.py
index 2597085c..efe070bb 100644
--- a/ikomia/dnn/dnntrain.py
+++ b/ikomia/dnn/dnntrain.py
@@ -59,17 +59,27 @@ def _init_mlflow(self):
         Internal use only
         """
         monitoring.check_mlflow_server()
-        # Create experiment
-        date_time_obj = datetime.now()
-        time_stamp_str = date_time_obj.strftime('%d-%m-%Y_%H:%M:%S')
         mlflow.set_tracking_uri(config.main_cfg["mlflow"]["tracking_uri"])
+        # Create experiment
+        self._create_mlflow_experiment()
 
+    def _create_mlflow_experiment(self):
         try:
+            date_time_obj = datetime.now()
+            time_stamp_str = date_time_obj.strftime('%d-%m-%Y_%H:%M:%S')
             self.experiment_id = mlflow.create_experiment('experiment_' + time_stamp_str)
         except Exception as e:
-            logger.warning("MLFlow can't be started so training metrics will not be monitor in it.")
+            self.experiment_id = -1
+            logger.warning("Unable to create MLFlow experiment. Please check for server startup errors.")
             logger.debug(e)
 
+    def _is_experiment_exists(self):
+        try:
+            current_exp = mlflow.get_experiment(str(self.experiment_id))
+            return current_exp.lifecycle_stage == "active"
+        except:
+            return False
+
     @staticmethod
     def _init_tensorboard():
         try:
@@ -89,6 +99,12 @@ def begin_task_run(self):
         mlflow.end_run()
 
         if self.experiment_id != -1:
+            # Check if experiment still exists
+            if not self._is_experiment_exists():
+                self._create_mlflow_experiment()
+                if self.experiment_id == -1:
+                    return
+
             mlflow.start_run(experiment_id=self.experiment_id, run_name=self.name)
 
             # Log parameters
diff --git a/ikomia/utils/__init__.py b/ikomia/utils/__init__.py
index 4375d382..c14f5a84 100644
--- a/ikomia/utils/__init__.py
+++ b/ikomia/utils/__init__.py
@@ -11,6 +11,7 @@
 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 # See the License for the specific language governing permissions and
 # limitations under the License.
+from ikomia.utils import update_sys_path
 from ikomia.utils import iklogger
 
 try:
@@ -41,8 +42,8 @@ def strtobool(val):
     """
     val = val.lower()
     if val in ('y', 'yes', 't', 'true', 'on', '1'):
-        return 1
+        return True
     elif val in ('n', 'no', 'f', 'false', 'off', '0'):
-        return 0
+        return False
     else:
         raise ValueError("invalid truth value %r" % (val,))
diff --git a/ikomia/utils/autocomplete.py b/ikomia/utils/autocomplete.py
index 8bace434..c2a89632 100644
--- a/ikomia/utils/autocomplete.py
+++ b/ikomia/utils/autocomplete.py
@@ -42,42 +42,68 @@ def _write_auto_complete(f, task_name="", task=None, local=True):
 
     if task is not None:
         task_name = task.name
+    elif local:
+        task = ikomia.ik_registry.create_algorithm(task_name)
 
-    if local:
-        if task is None:
-            task = ikomia.ik_registry.create_algorithm(task_name)
+    # Class definition
+    class_name = re.sub(forbid_char, "", task_name)
+    f.write(f"class {class_name}:\n")
 
+    if local:
+        # Algorithm already installed
         parameters = task.get_parameters()
-
-    # Function definition
-    function_name = re.sub(forbid_char, "", task_name)
-
-    if not local or len(parameters) == 0:
-        f.write(f"def {function_name}():\n")
-        f.write(f"    algo = ikomia.ik_registry.create_algorithm(\"{function_name}\", None)\n")
-        f.write(f"    return algo\n\n")
+        if len(parameters) == 0:
+            # __new__() return task object instance
+            f.write("    def __new__(cls):\n")
+            f.write(f"        algo = ikomia.ik_registry.create_algorithm(\"{task_name}\", None)\n")
+            f.write(f"        return algo\n\n")
+        else:
+            function_params = ""
+            params_dict = "{\n"
+
+            for param in parameters:
+                if keyword.iskeyword(param):
+                    param_var = f"{param}_"
+                else:
+                    param_var = param
+
+                if function_params:
+                    function_params += ", "
+
+                # Compute _new_() parameters
+                param_var = re.sub(forbid_char, "", param_var)
+                param_value = str(parameters[param]).replace("\\", "/")
+                function_params += f"{param_var}: str=\"{param_value}\""
+                # Compute parameters dict
+                params_dict += f"            \"{param}\": {param_var},\n"
+                # Static class variable for names
+                f.write(f"    {param_var} = \"{param_var}\"\n")
+
+            # __new__() return task object instance
+            params_dict += "        }"
+            f.write(f"\n    def __new__(cls, {function_params}):\n")
+            f.write(f"        algo = ikomia.ik_registry.create_algorithm(\"{task_name}\", None)\n")
+            f.write(f"        algo.set_parameters({params_dict})\n")
+            f.write(f"        return algo\n\n")
     else:
-        function_params = ""
-        params_dict = "{\n"
-
-        for param in parameters:
-            if keyword.iskeyword(param):
-                param_var = f"{param}_"
-            else:
-                param_var = param
-
-            if function_params:
-                function_params += ", "
-
-            param_var = re.sub(forbid_char, "", param_var)
-            function_params += f"{param_var}: str=\"{str(parameters[param])}\""
-            params_dict += f"        \"{param}\": {param_var},\n"
-
-        params_dict += "    }"
-        f.write(f"def {function_name}({function_params}):\n")
-        f.write(f"    algo = ikomia.ik_registry.create_algorithm(\"{function_name}\", None)\n")
-        f.write(f"    algo.set_parameters({params_dict})\n")
-        f.write(f"    return algo\n\n")
+        # Algorithm from Ikomia Hub
+        # __new__() return task object instance
+        f.write("    def __new__(cls, **kwargs):\n")
+        f.write(f"        algo = ikomia.ik_registry.create_algorithm(\"{task_name}\", None)\n")
+        f.write("        if algo is not None:\n")
+        f.write("            params = {}\n")
+        f.write("            for arg in kwargs:\n")
+        f.write("                if isinstance(kwargs[arg], str):\n")
+        f.write("                    params[arg] = kwargs[arg]\n")
+        f.write("                else:\n")
+        f.write("                    params[arg] = str(kwargs[arg])\n\n")
+        f.write("            algo.set_parameters(params)\n\n")
+        f.write(f"        return algo\n\n")
+
+    # name() return task name
+    f.write("    @staticmethod\n")
+    f.write("    def name():\n")
+    f.write(f"        return \"{task_name}\"\n\n")
 
 
 def _generate_python_file(folder):
@@ -102,36 +128,76 @@ def _generate_python_file(folder):
         importlib.reload(ikomia.utils.ik)
 
 
+def _is_valid_python_plugin_folder(name:str, folder_path:str):
+    main_python_file = os.path.join(folder_path, f"{name}.py")
+    return os.path.exists(main_python_file)
+
+
+def _is_valid_cpp_plugin_folder(name:str, folder_path:str):
+    for name in os.listdir(folder_path):
+        # Just check if at least one shared library exists -> maybe not sufficient...
+        filename, ext = os.path.splitext(name)
+        if ".so" in ext or ".dll" in ext:
+            return True
+
+    return False
+
+
 def _check_local_sync():
     if not _ik_auto_complete:
         return False
 
-    ik_names = dir(ik)
-    names = ikomia.ik_registry.get_algorithms()
+    # Get local algorithm names from plugin directories
+    local_folder_names = set()
+    python_plugin_folder = os.path.join(ikomia.ik_registry.get_plugins_directory(), "Python")
+    cpp_plugin_folder = os.path.join(ikomia.ik_registry.get_plugins_directory(), "C++")
 
-    for name in names:
-        if name not in ik_names:
-            return False
+    for name in os.listdir(python_plugin_folder):
+        folder_path = os.path.join(python_plugin_folder, name)
+        if os.path.isdir(folder_path) and _is_valid_python_plugin_folder(name, folder_path):
+            local_folder_names.add(name)
 
-    return True
+    for name in os.listdir(cpp_plugin_folder):
+        folder_path = os.path.join(cpp_plugin_folder, name)
+        if os.path.isdir(folder_path) and _is_valid_cpp_plugin_folder(name, folder_path):
+            local_folder_names.add(name)
+
+    # Get auto-completion local list
+    ik_names_set = set(ik.local_names)
+
+    # Check changes
+    if ik_names_set == local_folder_names:
+        return True
+    else:
+        # Local plugins has changed
+        diff_local = local_folder_names - ik_names_set
+
+        # Try to load them -> auto-completion has to be updated if at least one is valid
+        for name in diff_local:
+            algo = ikomia.ik_registry.create_algorithm(name=name, no_hub=True)
+            if algo is not None:
+                return False
+
+        return True
 
 
 def _check_online_sync():
     if not _ik_auto_complete:
         return False
 
-    ik_names = dir(ik)
-
     try:
         algos = ikomia.ik_registry.get_online_algorithms()
     except:
+        # Connection to Ikomia HUB failed
         return True
 
-    for algo in algos:
-        if algo["name"] not in ik_names:
-            return False
-
-    return True
+    try:
+        ik_names_set = set(ik.online_names)
+        online_names_set = {algo["name"] for algo in algos}
+        return ik_names_set == online_names_set
+    except:
+        # Auto-completion cache is in not complete
+        return False
 
 
 def _check_task_params(task):
@@ -139,27 +205,19 @@ def _check_task_params(task):
         return False
 
     params = task.get_parameters()
-    ik_content = inspect.getsource(ik)
-    regex_str = f"def {task.name}" + "\(([^\)]+)"
-    result = re.search(regex_str, ik_content)
 
-    if result is None:
-        return len(params) == 0
-    else:
-        params_dict_str = result.group(1)
-        params_groups = params_dict_str.split(", ")
-        param_keys = []
+    try:
+        ik_class = getattr(ik, task.name)
+    except:
+        return False
 
-        for group in params_groups:
-            result = re.search("(.+):", group)
-            if result is not None:
-                param_keys.append(result.group(1))
+    ik_class_vars = vars(ik_class)
 
-        for key in param_keys:
-            if key not in params.keys():
-                return False
+    for param_key in params:
+        if param_key not in ik_class_vars:
+            return False
 
-        return True
+    return True
 
 
 def _has_local_cache():
@@ -203,6 +261,14 @@ def make_local_plugins(force=False):
             return
 
     names = ikomia.ik_registry.get_algorithms()
+
+    # Write local names list declaration
+    f.write("local_names = [\n")
+    for name in names:
+        f.write(f"    '{name}',\n")
+    f.write(f"]\n\n")
+
+    # Write instanciation functions
     for name in names:
         try:
             _write_auto_complete(f, task_name=name, local=True)
@@ -228,8 +294,10 @@ def update_local_plugin(name):
     if info.internal:
         return
     elif not _check_task_params(task):
-        # TODO change only function definition instead of rebuild all
+        # TODO change only function definition instead of rebuild all?
         make_local_plugins(force=True)
+        # TODO remove function definition instead of rebuild all?
+        make_online_plugins(force=True)
     elif not _check_local_sync():
         cache_name = "autocomplete_local.cache"
 
@@ -240,13 +308,16 @@ def update_local_plugin(name):
         except Exception:
             try:
                 folder = os.path.join(site.getusersitepackages(), "ikomia", "utils")
-                cache_file_path2 = os.path.join(local_site, cache_name)
+                cache_file_path2 = os.path.join(folder, cache_name)
                 f = open(cache_file_path2, "a+")
             except Exception:
                 logger.warning("Ikomia auto-completion is disable: no update.")
 
         try:
             _write_auto_complete(f, task=task, local=True)
+            ik.local_names.append(task.name)
+            # TODO remove function definition instead of rebuild all?
+            make_online_plugins(force=True)
             f.close()
             _generate_python_file(folder)
             logger.info("Ikomia auto-completion updated for installed plugin.")
@@ -256,10 +327,7 @@ def update_local_plugin(name):
 
 
 def make_online_plugins(force=False):
-    if _check_online_sync():
-        return
-
-    if not force and _has_online_cache():
+    if not force and _has_online_cache() and _check_online_sync():
         return
 
     if not ikomia.ik_registry.is_all_loaded():
@@ -287,6 +355,13 @@ def make_online_plugins(force=False):
         logger.debug(e)
         return
 
+    # Write online names list declaration
+    f.write("online_names = [\n")
+    for algo in online_algos:
+        f.write(f"    '{algo['name']}',\n")
+    f.write(f"]\n\n")
+
+    # Write instanciation functions
     local_names = ikomia.ik_registry.get_algorithms()
     for algo in online_algos:
         if algo["name"] not in local_names:
diff --git a/ikomia/dataprocess/displayIO.py b/ikomia/utils/displayIO.py
similarity index 84%
rename from ikomia/dataprocess/displayIO.py
rename to ikomia/utils/displayIO.py
index 443304b6..c4c19ec8 100644
--- a/ikomia/dataprocess/displayIO.py
+++ b/ikomia/utils/displayIO.py
@@ -19,12 +19,15 @@
 import sys
 import os
 import logging
+import numpy
 import numpy as np
 from functools import singledispatch
 from ikomia import core, dataprocess
 import matplotlib
 import matplotlib.patches as patches
 from matplotlib.cbook import flatten
+from PIL import Image
+import cv2
 
 # Matplotlib backend choice
 _backend_name = None
@@ -68,7 +71,7 @@ def _to_plot_color(color):
 
 
 @singledispatch
-def display(obj, label="", fig=None, **kwargs):
+def display(obj, title: str = "", fig=None, **kwargs):
     """
     Display function for various workflow components.
     """
@@ -76,7 +79,7 @@ def display(obj, label="", fig=None, **kwargs):
 
 
 @display.register
-def _(obj: dataprocess.CImageIO, label="", fig=None, **kwargs):
+def _(obj: dataprocess.CImageIO, title: str = "", fig=None, **kwargs):
     """
     Display image input or output (:py:class:`~ikomia.dataprocess.pydataprocess.CImageIO`) in a Matplotlib figure.
     """
@@ -92,7 +95,7 @@ def _(obj: dataprocess.CImageIO, label="", fig=None, **kwargs):
         child = False
         fig, ax = plt.subplots(1, 1)
 
-    ax.set_title(label)
+    ax.set_title(title)
     ax.imshow(obj.get_image())
     ax.axis("off")
 
@@ -102,7 +105,7 @@ def _(obj: dataprocess.CImageIO, label="", fig=None, **kwargs):
 
 
 @display.register
-def _(obj: dataprocess.CGraphicsInput, label="", fig=None, **kwargs):
+def _(obj: dataprocess.CGraphicsInput, title: str = "", fig=None, **kwargs):
     """
     Display the scene of a graphics input (:py:class:`~ikomia.dataprocess.pydataprocess.CGraphicsInput`)
     in a Matplotlib figure.
@@ -119,7 +122,7 @@ def _(obj: dataprocess.CGraphicsInput, label="", fig=None, **kwargs):
         child = False
         fig, ax = plt.subplots(1, 1)
 
-    ax.set_title(label)
+    ax.set_title(title)
     items = obj.get_items()
     x_min = y_min = sys.maxsize
     x_max = y_max = 0
@@ -219,7 +222,7 @@ def update_min_max(x, y):
 
 
 @display.register
-def _(obj: dataprocess.CGraphicsOutput, label="", fig=None, **kwargs):
+def _(obj: dataprocess.CGraphicsOutput, title: str = "", fig=None, **kwargs):
     """
     Display the scene of a graphics output (:py:class:`~ikomia.dataprocess.pydataprocess.CGraphicsOutput`)
     in a Matplotlib figure.
@@ -236,7 +239,7 @@ def _(obj: dataprocess.CGraphicsOutput, label="", fig=None, **kwargs):
         child = False
         fig, ax = plt.subplots(1, 1)
 
-    ax.set_title(label)
+    ax.set_title(title)
     items = obj.get_items()
     x_min = y_min = sys.maxsize
     x_max = y_max = 0
@@ -336,7 +339,7 @@ def update_min_max(x, y):
 
 
 @display.register
-def _(obj: dataprocess.CNumericIO, label="", fig=None, **kwargs):
+def _(obj: dataprocess.CNumericIO, title: str = "", fig=None, **kwargs):
     """
     Display numeric values input or output (:py:class:`~ikomia.dataprocess.pydataprocess.CNumericIO`)
     as a table in a Matplotlib figure.
@@ -481,7 +484,7 @@ def _(obj: dataprocess.CNumericIO, label="", fig=None, **kwargs):
                 else:
                     ax[i].pie(x=values[i], labels=labels[i], shadow=True)
 
-    fig.suptitle(label)
+    fig.suptitle(title)
 
     if not child:
         fig.tight_layout()
@@ -489,7 +492,7 @@ def _(obj: dataprocess.CNumericIO, label="", fig=None, **kwargs):
 
 
 @display.register
-def _(obj: dataprocess.CBlobMeasureIO, label="", fig=None, **kwargs):
+def _(obj: dataprocess.CBlobMeasureIO, title: str = "", fig=None, **kwargs):
     """
     Display object measures input or output (:py:class:`~ikomia.dataprocess.pydataprocess.CBlobMeasureIO`)
     as a table in a Matplotlib figure.
@@ -553,7 +556,7 @@ def _(obj: dataprocess.CBlobMeasureIO, label="", fig=None, **kwargs):
     table.set_fontsize(10)
 
     ax.axis("off")
-    fig.suptitle(label)
+    fig.suptitle(title)
 
     if not child:
         fig.tight_layout()
@@ -561,7 +564,84 @@ def _(obj: dataprocess.CBlobMeasureIO, label="", fig=None, **kwargs):
 
 
 @display.register
-def _(obj: dataprocess.CWorkflowTask, label="", **kwargs):
+def _(obj: dataprocess.CObjectDetectionIO, title: str = "", **kwargs):
+    """
+    Display object detection input or output
+    (:py:class:`~ikomia.dataprocess.pydataprocess.CObjectDetectionIO`).
+    """
+    _check_backend()
+
+    if not obj.is_data_available():
+        return
+
+    display(obj.getGraphicsIO(), f"{title}:graphics objects")
+    display(obj.getBlobMeasureIO(), f"{title}:results")
+
+
+@display.register
+def _(obj: dataprocess.CInstanceSegmentationIO, title: str = "", **kwargs):
+    """
+    Display instance segmentation input or output
+    (:py:class:`~ikomia.dataprocess.pydataprocess.CInstanceSegmentationIO`).
+    """
+    _check_backend()
+
+    if not obj.is_data_available():
+        return
+
+    display(obj.getMaskImageIO(), f"{title}:segmentation mask")
+    display(obj.getGraphicsIO(), f"{title}:graphics objects")
+    display(obj.getBlobMeasureIO(), f"{title}:results")
+
+
+@display.register
+def _(obj: dataprocess.CSemanticSegmentationIO, title: str = "", **kwargs):
+    """
+    Display semantic segmentation input or output
+    (:py:class:`~ikomia.dataprocess.pydataprocess.CSemanticSegmentationIO`).
+    """
+    _check_backend()
+
+    if not obj.is_data_available():
+        return
+
+    display(obj.getMaskImageIO(), f"{title}:segmentation mask")
+    display(obj.getLegendImageIO(), f"{title}:legend")
+
+
+@display.register
+def _(obj: dataprocess.CKeypointsIO, title: str = "", **kwargs):
+    """
+    Display keypoints input or output
+    (:py:class:`~ikomia.dataprocess.pydataprocess.CKeypointsIO`).
+    """
+    _check_backend()
+
+    if not obj.is_data_available():
+        return
+
+    display(obj.getGraphicsIO(), f"{title}:graphics objects")
+    display(obj.getBlobMeasureIO(), f"{title}:results")
+    display(obj.getDataStringIO(), f"{title}:keypoint links")
+
+
+@display.register
+def _(obj: dataprocess.CTextIO, title: str = "", **kwargs):
+    """
+    Display text input or output
+    (:py:class:`~ikomia.dataprocess.pydataprocess.CTextIO`).
+    """
+    _check_backend()
+
+    if not obj.is_data_available():
+        return
+
+    display(obj.getGraphicsIO(), f"{title}:graphics objects")
+    display(obj.getDataStringIO(), f"{title}:text fields")
+
+
+@display.register
+def _(obj: dataprocess.CWorkflowTask, title: str = "", **kwargs):
     """
     Display task inputs and outputs (:py:class:`~ikomia.core.pycore.CWorkflowTask`) in two separate Matplotlib figures.
     """
@@ -571,7 +651,7 @@ def _(obj: dataprocess.CWorkflowTask, label="", **kwargs):
     inputs = obj.get_inputs()
     input_count = len(inputs)
     in_fig = plt.figure(num=1, constrained_layout=True)
-    in_fig.suptitle(label + " inputs")
+    in_fig.suptitle(title + " inputs")
     in_sub_figs = in_fig.subfigures(input_count, 1, wspace=0.07)
 
     if input_count == 1:
@@ -590,7 +670,7 @@ def _(obj: dataprocess.CWorkflowTask, label="", **kwargs):
     outputs = obj.get_outputs()
     output_count = len(outputs)
     out_fig = plt.figure(num=2, constrained_layout=True)
-    out_fig.suptitle(label + " outputs")
+    out_fig.suptitle(title + " outputs")
     out_sub_figs = out_fig.subfigures(output_count, 1, wspace=0.07)
 
     if output_count == 1:
@@ -609,7 +689,7 @@ def _(obj: dataprocess.CWorkflowTask, label="", **kwargs):
 
 
 @display.register
-def _(obj: dataprocess.CWorkflow, label="", **kwargs):
+def _(obj: dataprocess.CWorkflow, title: str = "", **kwargs):
     """
     Display Graphviz representation of a workflow (:py:class:`~ikomia.dataprocess.pydataprocess.CWorkflow`).
     """
@@ -620,3 +700,28 @@ def _(obj: dataprocess.CWorkflow, label="", **kwargs):
     s = Source.from_file(path)
     s.view()
 
+
+@display.register
+def _(obj: numpy.ndarray, title: str = "", **kwargs):
+    if "viewer" not in kwargs or kwargs["viewer"] == "pillow":
+        _display_pillow(obj, title, **kwargs)
+    else:
+        _display_opencv(obj, title, **kwargs)
+
+
+def _display_pillow(image: numpy.ndarray, title: str = "", **kwargs):
+    try:
+        img_in = Image.fromarray(image)
+        img_in.show(title)
+    except Exception as e:
+        # Fallback to OpenCV
+        logger.debug(f"Unable to use Pillow for display: {e}")
+        _display_opencv(image, title, **kwargs)
+
+
+def _display_opencv(image: numpy.ndarray, title: str = "", **kwargs):
+    disp_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
+    cv2.imshow(title, disp_image)
+
+    if "wait_key" in kwargs:
+        cv2.waitKey(kwargs["wait_key"])
diff --git a/ikomia/utils/http.py b/ikomia/utils/http.py
index ed96668b..5358c374 100644
--- a/ikomia/utils/http.py
+++ b/ikomia/utils/http.py
@@ -22,7 +22,7 @@
 logger = logging.getLogger(__name__)
 
 
-def http_except(func):
+def http_no_raise(func):
     @functools.wraps(func)
     def wrapper(*argc, **kwargs):
         try:
@@ -39,7 +39,7 @@ def wrapper(*argc, **kwargs):
     return wrapper
 
 
-@http_except
+@http_no_raise
 def download_file(url, path, public=True):
     if public:
         with requests.get(url, stream=True) as r:
@@ -56,7 +56,7 @@ def download_file(url, path, public=True):
     else:
         s = ikomia.ik_api_session
         if s.token is None:
-            logger.error("Download failed, authentication required.")
+            logger.error("Download private file failed, authentication required.")
             return
 
         with s.session.get(url, stream=True) as r:
diff --git a/ikomia/utils/iklogger.py b/ikomia/utils/iklogger.py
index c0d91113..2f94765b 100644
--- a/ikomia/utils/iklogger.py
+++ b/ikomia/utils/iklogger.py
@@ -5,7 +5,7 @@
 # ----------------------------------
 # ----- Logging initialization -----
 # ----------------------------------
-logger = logging.getLogger(__name__)
+logger = logging.getLogger("ikomia")
 logger.handlers = []
 logger.setLevel(logging.DEBUG)
 
diff --git a/ikomia/utils/update_sys_path.py b/ikomia/utils/update_sys_path.py
new file mode 100644
index 00000000..69384324
--- /dev/null
+++ b/ikomia/utils/update_sys_path.py
@@ -0,0 +1,16 @@
+# For Windows users who want to use the standalone API in "dev mode"
+# The user environment variable IKOMIA_DEV = 1 must be set
+
+import os
+import sys
+
+if sys.platform == "win32":
+    try:
+        if os.environ["IKOMIA_DEV"] == "1":
+            dll_list = os.environ["PATH"]
+            dlls = dll_list.split(";")
+            for dll in dlls:
+                if dll:
+                    os.add_dll_directory(dll)
+    except:
+        pass
\ No newline at end of file
diff --git a/setup.cfg b/setup.cfg
index 1033eb6b..f11d8001 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -38,13 +38,13 @@ python_requires = >=3.7
 install_requires =
     cython
     setuptools == 59.5.0
-    numpy >= 1.20.3
+    numpy >= 1.20.3, < 1.24
     requests
-    mlflow == 1.24.0
-    tensorboard >= 2.5.0
+    mlflow == 1.30.0
+    tensorboard >= 2.5.0, < 3.0
     Pillow >= 8.1.0
-    tqdm >= 4.41.0
-    matplotlib == 3.4.3
+    tqdm >= 4.41.0, < 5.0
+    matplotlib >= 3.4.3, <4.0
     python-dotenv >= 0.18.0
     pyyaml
 


Test patch:
diff --git a/tests/data/workflows/WorkflowTestOutput.json b/tests/data/workflows/WorkflowTestOutput.json
index 7592ed8f..209c9d4e 100644
--- a/tests/data/workflows/WorkflowTestOutput.json
+++ b/tests/data/workflows/WorkflowTestOutput.json
@@ -1,5 +1,9 @@
 {
-    "connections": [{
+    "api": {
+        "version": "0.8.1"
+    },
+    "connections": [
+        {
             "source_id": -1,
             "source_index": 0,
             "target_id": 0,
@@ -16,6 +20,18 @@
             "source_index": 0,
             "target_id": 2,
             "target_index": 0
+        },
+        {
+            "source_id": -1,
+            "source_index": 0,
+            "target_id": 3,
+            "target_index": 0
+        },
+        {
+            "source_id": -1,
+            "source_index": 0,
+            "target_id": 4,
+            "target_index": 0
         }
     ],
     "metadata": {
@@ -23,10 +39,13 @@
         "keywords": "",
         "name": "MultiOutputTypes"
     },
-    "tasks": [{
+    "tasks": [
+        {
             "task_data": {
+                "language": 0,
                 "name": "infer_mobilenet_ssd",
-                "parameters": [{
+                "parameters": [
+                    {
                         "name": "structureFile",
                         "value": "/home/ludo/Ikomia/Plugins/C++/MobileNetSSD/Model/mobileNetSSD.prototxt"
                     },
@@ -70,24 +89,30 @@
                         "name": "backend",
                         "value": "0"
                     }
-                ]
+                ],
+                "url": "file:///home/ludo/Ikomia/Plugins/C++/infer_mobilenet_ssd"
             },
             "task_id": 0
         },
         {
             "task_data": {
+                "language": 0,
                 "name": "ocv_split",
-                "parameters": [{
-                    "name": "outputCount",
-                    "value": "3"
-                }]
+                "parameters": [
+                    {
+                        "name": "outputCount",
+                        "value": "3"
+                    }
+                ]
             },
             "task_id": 1
         },
         {
             "task_data": {
+                "language": 0,
                 "name": "ocv_calc_hist",
-                "parameters": [{
+                "parameters": [
+                    {
                         "name": "nChannels",
                         "value": "1"
                     },
@@ -121,10 +146,103 @@
         },
         {
             "task_data": {
-                "name": "dataset_wgisd",
-                "parameters": []
+                "language": 0,
+                "name": "infer_inception_v3",
+                "parameters": [
+                    {
+                        "name": "structureFile",
+                        "value": ""
+                    },
+                    {
+                        "name": "labelsFile",
+                        "value": "/home/ludo/Ikomia/Plugins/C++/infer_inception_v3/Model/imagenet_names.txt"
+                    },
+                    {
+                        "name": "modelFile",
+                        "value": "/home/ludo/Ikomia/Plugins/C++/infer_inception_v3/Model/tensorflow_inception_graph.pb"
+                    },
+                    {
+                        "name": "datasetName",
+                        "value": ""
+                    },
+                    {
+                        "name": "inputSize",
+                        "value": "416"
+                    },
+                    {
+                        "name": "framework",
+                        "value": "0"
+                    },
+                    {
+                        "name": "modelName",
+                        "value": ""
+                    },
+                    {
+                        "name": "target",
+                        "value": "0"
+                    },
+                    {
+                        "name": "backend",
+                        "value": "0"
+                    }
+                ],
+                "url": "file:///home/ludo/Ikomia/Plugins/C++/infer_inception_v3"
             },
             "task_id": 3
+        },
+        {
+            "task_data": {
+                "language": 0,
+                "name": "infer_mask_rcnn",
+                "parameters": [
+                    {
+                        "name": "maskThreshold",
+                        "value": "0.300000"
+                    },
+                    {
+                        "name": "structureFile",
+                        "value": "/home/ludo/Ikomia/Plugins/C++/infer_mask_rcnn/Model/mask_rcnn_inception_v2_coco_2018_01_28.pbtxt"
+                    },
+                    {
+                        "name": "labelsFile",
+                        "value": "/home/ludo/Ikomia/Plugins/C++/infer_mask_rcnn/Model/coco_names.txt"
+                    },
+                    {
+                        "name": "modelFile",
+                        "value": "/home/ludo/Ikomia/Plugins/C++/infer_mask_rcnn/Model/frozen_inference_graph.pb"
+                    },
+                    {
+                        "name": "datasetName",
+                        "value": ""
+                    },
+                    {
+                        "name": "inputSize",
+                        "value": "416"
+                    },
+                    {
+                        "name": "confidence",
+                        "value": "0.500000"
+                    },
+                    {
+                        "name": "framework",
+                        "value": "0"
+                    },
+                    {
+                        "name": "modelName",
+                        "value": ""
+                    },
+                    {
+                        "name": "target",
+                        "value": "0"
+                    },
+                    {
+                        "name": "backend",
+                        "value": "0"
+                    }
+                ],
+                "url": "file:///home/ludo/Ikomia/Plugins/C++/infer_mask_rcnn"
+            },
+            "task_id": 4
         }
     ]
-}
\ No newline at end of file
+}
diff --git a/tests/test-display.py b/tests/test-display.py
index c8b57a31..50fdb077 100644
--- a/tests/test-display.py
+++ b/tests/test-display.py
@@ -2,8 +2,8 @@
 import ikomia
 import cv2
 from ikomia import core, dataprocess
-from ikomia.dataprocess import displayIO, workflow
-from ikomia.utils import tests, ik
+from ikomia.dataprocess import workflow
+from ikomia.utils import tests, ik, displayIO
 
 logger = logging.getLogger(__name__)
 
diff --git a/tests/test-hub.py b/tests/test-hub.py
new file mode 100644
index 00000000..afc32086
--- /dev/null
+++ b/tests/test-hub.py
@@ -0,0 +1,75 @@
+import logging
+import argparse
+from ikomia.utils import tests, ik
+from ikomia.dataprocess import workflow
+
+logger = logging.getLogger(__name__)
+
+
+def test_resnet_train(dataset_dir):
+    logger.info("===== Test::launch ResNet training =====")
+    wf_path = tests.get_test_workflow_directory() + "/WorkflowResNetTrain.json"
+    wf = workflow.load(wf_path)
+    logger.info("Start ResNet training...")
+    wf.run_on(folder=dataset_dir)
+    logger.info("Training finished successfully")
+
+
+def test_yolov5_train(wgisd_dataset_dir):
+    logger.info("===== Test::launch YoloV5 training =====")
+    wf_path = tests.get_test_workflow_directory() + "/WorkflowYoloV5Train.json"
+    wf = workflow.load(wf_path)
+
+    # set dataset directory
+    wgisd_id, wgisd = wf.find_task(ik.dataset_wgisd)
+    wgisd_params = wgisd.get_parameters()
+    wgisd_params[ik.dataset_wgisd_param.data_folder_path] = wgisd_dataset_dir + "/data"
+    wgisd_params[ik.dataset_wgisd_param.class_file_path] = wgisd_dataset_dir + "/classes.txt"
+    wgisd.set_parameters(wgisd_params)
+
+    logger.info("Start YoloV5 training...")
+    wf.run()
+    logger.info("Training finished successfully")
+
+
+def test_yolo_train(wgisd_dataset_dir):
+    logger.info("===== Test::launch Darknet YOLO training =====")
+    wf = workflow.create("YoloTrain")
+    wgisd = wf.add_task(ik.dataset_wgisd)
+    wgisd_params = wgisd.get_parameters()
+    wgisd_params[ik.dataset_wgisd_param.data_folder_path] = wgisd_dataset_dir + "/data"
+    wgisd_params[ik.dataset_wgisd_param.class_file_path] = wgisd_dataset_dir + "/classes.txt"
+    wgisd_params[ik.dataset_wgisd_param.seg_mask_mode] = "None"
+    wgisd.set_parameters(wgisd_params)
+
+    yolo = wf.add_task(ik.train_yolo)
+    yolo_params = yolo.get_parameters()
+    yolo_params["batchSize"] = 16
+    task.set_parameters(yolo, yolo_params)
+    wf.connect_tasks(wgisd, yolo)
+
+    wf.run()
+
+
+if __name__ == "__main__":
+    parser = argparse.ArgumentParser()
+    parser.add_argument("--classif_dataset_dir",
+                        type=str,
+                        default="/home/ludo/Images/Datasets/hymenoptera_data", help="Classification datatset folder")
+    parser.add_argument("--detect_dataset_dir",
+                        type=str,
+                        default="/home/ludo/Images/Datasets/wgisd", help="Object detection datatset folder")
+    parser.add_argument("--tests",
+                        type=str,
+                        default='all',
+                        help="List of tests to execute (comma-separated string, default=all)")
+    opt = parser.parse_args()
+    running_tests = opt.tests.split(',')
+
+    if 'all' in running_tests or 'train_resnet' in running_tests:
+        test_resnet_train(opt.classif_dataset_dir)
+    if 'all' in running_tests or 'train_yolov4' in running_tests:
+        test_yolo_train(opt.detect_dataset_dir)
+    if 'all' in running_tests or 'train_yolov5' in running_tests:
+        test_yolov5_train(opt.detect_dataset_dir)
+
diff --git a/tests/test-registry.py b/tests/test-registry.py
index 0bd492be..4baeceda 100644
--- a/tests/test-registry.py
+++ b/tests/test-registry.py
@@ -3,8 +3,7 @@
 import argparse
 import ikomia
 import cv2
-from ikomia.dataprocess import displayIO
-from ikomia.utils import tests, ik
+from ikomia.utils import tests, ik, displayIO
 
 logger = logging.getLogger(__name__)
 
diff --git a/tests/test-task-io.py b/tests/test-task-io.py
index 48db35fc..cc378518 100644
--- a/tests/test-task-io.py
+++ b/tests/test-task-io.py
@@ -60,8 +60,6 @@ def test_cpp_base_class():
     assert type(elt_count) == int
     data_available = io.is_data_available()
     assert data_available is False
-    auto_input = io.is_auto_input()
-    assert auto_input is False
     composite = io.is_composite()
     assert composite is False
     assert io.displayable is True
diff --git a/tests/test-task.py b/tests/test-task.py
index 94eff129..0d7a44df 100644
--- a/tests/test-task.py
+++ b/tests/test-task.py
@@ -1,9 +1,9 @@
 import os.path
 import logging
 import argparse
-from ikomia.utils import tests, ik
+from ikomia.utils import tests, ik, displayIO
 from ikomia import core, dataio, dataprocess
-from ikomia.dataprocess import displayIO, workflow
+from ikomia.dataprocess import workflow
 import cv2
 import numpy as np
 
diff --git a/tests/test-workflow.py b/tests/test-workflow.py
index d186b7a5..7b231841 100644
--- a/tests/test-workflow.py
+++ b/tests/test-workflow.py
@@ -3,8 +3,9 @@
 import shutil
 import argparse
 import ikomia
-from ikomia.dataprocess import CWorkflow, CImageIO, workflow, displayIO
-from ikomia.core import config, task, IODataType
+from ikomia.dataprocess import \
+    workflow, CWorkflow, CImageIO, CVideoIO, CPathIO, CNumericIO, CObjectDetectionIO, CInstanceSegmentationIO
+from ikomia.core import IODataType
 from ikomia.utils import tests, ik
 import numpy as np
 import cv2
@@ -13,6 +14,9 @@
 
 
 def test_cpp_workflow():
+    """
+    Test Python bindings of Ikomia Core CWorkflow
+    """
     logger.info("===== Test: CWorkflow =====")
 
     # Default ctor
@@ -152,227 +156,470 @@ def test_cpp_workflow():
     os.remove(save_graphviz)
 
 
-def test_load_builtin():
-    # load test workflow
-    logger.info("===== Test::load workflow from JSON with builtin algorithms =====")
-    wf_path = tests.get_test_workflow_directory() + "/WorkflowTest1.json"
+def test_ctor():
+    logger.info("===== Test: Workflow constructor=====")
+    name = "Test workflow"
+
+    wf = workflow.Workflow()
+    assert wf.name == "Untitled"
+    assert wf.registry is not None
+    assert wf.output_folder
+    assert wf.root_uuid
+    assert len(wf.task_to_id) > 0
+
+    wf = workflow.Workflow(name)
+    assert wf.name == name
+    assert wf.registry is not None
+    assert wf.output_folder
+    assert wf.root_uuid
+    assert len(wf.task_to_id) > 0
+
+
+def test_root():
+    logger.info("===== Test: Workflow::root() =====")
+    name = "Test workflow"
+    wf = workflow.Workflow(name)
+    root = wf.root()
+    assert root is not None
+    assert wf.get_task_id(root) == wf.get_root_id()
+
+
+def test_set_image_input():
+    logger.info("===== Test: Workflow::set_image_input() =====")
+    name = "Test workflow"
+    img_path = os.path.join(tests.get_test_image_directory(), "Lena.png")
+    img_url = "https://helpx.adobe.com/content/dam/help/en/photoshop/using/convert-color-image-black-white/jcr_content/main-pars/before_and_after/image-before/Landscape-Color.jpg"
+
+    # From path
+    wf = workflow.Workflow(name)
+    wf.set_image_input(path=img_path)
+    assert wf.get_input_count() == 1
+    img_in = wf.get_input(0)
+    assert type(img_in) == CImageIO
+    assert img_in.data_type == IODataType.IMAGE
+    assert img_in.is_data_available()
+    assert img_in.get_image() is not None
+
+    # From URL
+    wf = workflow.Workflow(name)
+    wf.set_image_input(url=img_url)
+    assert wf.get_input_count() == 1
+    img_in = wf.get_input(0)
+    assert type(img_in) == CImageIO
+    assert img_in.data_type == IODataType.IMAGE
+    assert img_in.is_data_available()
+    assert img_in.get_image() is not None
+
+    # From array
+    img_array = np.random.randint(low=0, high=255, size=(512, 512, 3), dtype=np.uint8)
+    wf = workflow.Workflow(name)
+    wf.set_image_input(array=img_array)
+    assert wf.get_input_count() == 1
+    img_in = wf.get_input(0)
+    assert type(img_in) == CImageIO
+    assert img_in.data_type == IODataType.IMAGE
+    assert img_in.is_data_available()
+    assert img_in.get_image() is not None
+
+    # Data type
+    wf = workflow.Workflow(name)
+    wf.set_image_input(path=img_path, datatype=IODataType.IMAGE_LABEL)
+    img_in = wf.get_input(0)
+    assert type(img_in) == CImageIO
+    assert img_in.data_type == IODataType.IMAGE_LABEL
+
+    # Index
+    wf = workflow.Workflow(name)
+    wf.set_image_input(path=img_path, index=-1)
+    assert wf.get_input_count() == 1
+    img_in = wf.get_input(0)
+    assert type(img_in) == CImageIO
+    wf.set_image_input(path=img_path, index=0)
+    assert wf.get_input_count() == 1
+    img_in = wf.get_input(0)
+    assert type(img_in) == CImageIO
+    wf.set_image_input(path=img_path, index=1)
+    assert wf.get_input_count() == 2
+    img_in = wf.get_input(1)
+    assert type(img_in) == CImageIO
+    wf.set_image_input(path=img_path, index=10)
+    assert wf.get_input_count() == 3
+    img_in = wf.get_input(2)
+    assert type(img_in) == CImageIO
+
+
+def test_set_video_input():
+    logger.info("===== Test: Workflow::set_video_input() =====")
+    name = "Test workflow"
+    video_path = os.path.join(tests.get_test_video_directory(), "basketball.mp4")
+    video_url = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4"
+
+    # From path
+    wf = workflow.Workflow(name)
+    wf.set_video_input(path=video_path)
+    assert wf.get_input_count() == 1
+    video_in = wf.get_input(0)
+    assert type(video_in) == CVideoIO
+    assert video_in.data_type == IODataType.VIDEO
+    assert video_in.has_video()
+
+    # From URL
+    wf = workflow.Workflow(name)
+    wf.set_video_input(url=video_url)
+    assert wf.get_input_count() == 1
+    video_in = wf.get_input(0)
+    assert type(video_in) == CVideoIO
+    assert video_in.data_type == IODataType.VIDEO
+    assert video_in.has_video()
+
+    # Data type
+    wf = workflow.Workflow(name)
+    wf.set_video_input(path=video_path, datatype=IODataType.VIDEO_LABEL)
+    video_in = wf.get_input(0)
+    assert type(video_in) == CVideoIO
+    assert video_in.data_type == IODataType.VIDEO_LABEL
+
+    # Index
+    wf = workflow.Workflow(name)
+    wf.set_video_input(path=video_path, index=-1)
+    assert wf.get_input_count() == 1
+    video_in = wf.get_input(0)
+    assert type(video_in) == CVideoIO
+    wf.set_video_input(path=video_path, index=0)
+    assert wf.get_input_count() == 1
+    video_in = wf.get_input(0)
+    assert type(video_in) == CVideoIO
+    wf.set_video_input(path=video_path, index=1)
+    assert wf.get_input_count() == 2
+    video_in = wf.get_input(1)
+    assert type(video_in) == CVideoIO
+    wf.set_video_input(path=video_path, index=10)
+    assert wf.get_input_count() == 3
+    video_in = wf.get_input(2)
+    assert type(video_in) == CVideoIO
+
+
+def test_set_directory_input():
+    logger.info("===== Test: Workflow::set_directory_input() =====")
+    name = "Test workflow"
+    dir_path = tests.get_test_image_directory()
+    wf = workflow.Workflow(name)
+    wf.set_directory_input(dir_path)
+    assert wf.get_input_count() == 1
+    dir_in = wf.get_input(0)
+    assert type(dir_in) == CPathIO
+    assert dir_in.data_type == IODataType.FOLDER_PATH
+    assert dir_in.is_data_available()
+
+    # Index
+    wf = workflow.Workflow(name)
+    wf.set_directory_input(dir_path, index=-1)
+    assert wf.get_input_count() == 1
+    dir_in = wf.get_input(0)
+    assert type(dir_in) == CPathIO
+    wf.set_directory_input(dir_path, index=0)
+    assert wf.get_input_count() == 1
+    dir_in = wf.get_input(0)
+    assert type(dir_in) == CPathIO
+    wf.set_directory_input(dir_path, index=1)
+    assert wf.get_input_count() == 2
+    dir_in = wf.get_input(1)
+    assert type(dir_in) == CPathIO
+    wf.set_directory_input(dir_path, index=10)
+    ins = wf.get_inputs()
+    assert wf.get_input_count() == 3
+    dir_in = wf.get_input(2)
+    assert type(dir_in) == CPathIO
+
+
+def test_set_parameters():
+    logger.info("===== Test: Workflow::set_parameters() =====")
+    wf_path = os.path.join(tests.get_test_workflow_directory(), "WorkflowTest1.json")
     wf = workflow.load(wf_path)
-    logger.info("----- Workflow information:")
-    logger.info(wf.name)
-    logger.info(wf.description)
-    logger.info(wf.keywords)
-    logger.info("Input count: " + str(wf.get_input_count()))
-    logger.info("Task count: " + str(wf.get_task_count()))
+
+    # From task object
+    box_filter = wf.find_task(name=ik.ocv_box_filter.name(), index=0)
+    logger.info(box_filter.get_param_object())
+    params = {ik.ocv_box_filter.kSizeHeight: "11", ik.ocv_box_filter.kSizeWidth: "11"}
+    wf.set_parameters(params, task_obj=box_filter)
+    params_new = box_filter.get_parameters()
+    assert params_new[ik.ocv_box_filter.kSizeHeight] == params[ik.ocv_box_filter.kSizeHeight]
+    assert params_new[ik.ocv_box_filter.kSizeWidth] == params[ik.ocv_box_filter.kSizeWidth]
+
+    # From task name
+    bilateral_filter = wf.find_task(name=ik.ocv_bilateral_filter.name(), index=0)
+    logger.info(bilateral_filter.get_param_object())
+    params = {ik.ocv_bilateral_filter.sigmaSpace: "31.0", ik.ocv_bilateral_filter.sigmaColor: "11.0"}
+    wf.set_parameters(params, task_name=ik.ocv_bilateral_filter.name())
+    params_new = bilateral_filter.get_parameters()
+    assert float(params_new[ik.ocv_bilateral_filter.sigmaSpace]) == float(params[ik.ocv_bilateral_filter.sigmaSpace])
+    assert float(params_new[ik.ocv_bilateral_filter.sigmaColor]) == float(params[ik.ocv_bilateral_filter.sigmaColor])
 
 
-def test_load_marketplace():
-    # load test workflow
-    logger.info("===== Test::load workflow from JSON with marketplace algorithm =====")
-    wf_path = tests.get_test_workflow_directory() + "/WorkflowYolorBlur.json"
+def test_load():
+    logger.info("===== Test: Workflow::load() =====")
+    wf_path = os.path.join(tests.get_test_workflow_directory(), "WorkflowTest1.json")
     wf = workflow.load(wf_path)
+
     logger.info("----- Workflow information:")
     logger.info(wf.name)
     logger.info(wf.description)
     logger.info(wf.keywords)
-    logger.info("Input count: " + str(wf.get_input_count()))
     logger.info("Task count: " + str(wf.get_task_count()))
 
+    assert wf.name
+    assert wf.get_task_count() == 10
 
-def test_run_single_image():
-    logger.info("===== Test::execute workflow on single image =====")
-    img_path = tests.get_test_image_directory() + "/Lena.png"
-    wf_path = tests.get_test_workflow_directory() + "/WorkflowTest1.json"
-    wf = workflow.load(wf_path)
-    wf.set_auto_save(True)
+    ids = wf.get_task_ids()
+    for uuid in wf.task_to_id:
+        assert wf.task_to_id[uuid] in ids
 
-    # apply on local image
+
+def _check_leaf_outputs(wf):
+    leaf_tasks = wf.get_final_tasks()
+    for t in leaf_tasks:
+        outputs = t.get_outputs()
+        for output in outputs:
+            assert output.is_data_available()
+
+
+def test_run():
+    logger.info("===== Test: Workflow::run() =====")
+
+    # Image
+    logger.info("Run on image...")
+    img_path = os.path.join(tests.get_test_image_directory(), "Lena.png")
+    wf_path = os.path.join(tests.get_test_workflow_directory(), "WorkflowTest1.json")
+    wf = workflow.load(wf_path)
     wf.set_image_input(path=img_path)
-    logger.info("Start workflow on local image...")
     wf.run()
+    _check_leaf_outputs(wf)
     logger.info("Workflow finished successfully")
 
-    # apply on web image
-    img_url = "https://helpx.adobe.com/content/dam/help/en/photoshop/using/convert-color-image-black-white/jcr_content/main-pars/before_and_after/image-before/Landscape-Color.jpg"
-    wf.set_image_input(index=0, url=img_url)
-    logger.info("Start workflow on image url...")
+    # Video
+    logger.info("Run on video...")
+    video_path = os.path.join(tests.get_test_video_directory(), "basketball.mp4")
+    wf_path = os.path.join(tests.get_test_workflow_directory(), "WorkflowTestVideo.json")
+    wf = workflow.load(wf_path)
+    wf.set_video_input(path=video_path)
+    wf.set_cfg_entry("WholeVideo", str(int(True)))
     wf.run()
+    _check_leaf_outputs(wf)
     logger.info("Workflow finished successfully")
 
-    # apply on Numpy array
-    img_array = np.random.randint(low=0, high=255, size=(512, 512, 3), dtype=np.uint8)
-    wf.set_image_input(index=0, array=img_array)
-    logger.info("Start workflow on Numpy array...")
+    # Image folder
+    logger.info("Run on image folder...")
+    wf_path = tests.get_test_workflow_directory() + "/WorkflowTest1.json"
+    wf = workflow.load(wf_path)
+    wf.set_auto_save(True)
+    dir_path = tests.get_test_image_directory()
+    wf.set_directory_input(dir_path)
     wf.run()
     logger.info("Workflow finished successfully")
 
-
-def test_run_image_folder():
-    logger.info("===== Test::execute image workflow on folder =====")
-    wf_path = tests.get_test_workflow_directory() + "/WorkflowTest1.json"
+    # Video folder
+    logger.info("Run on video folder...")
+    wf_path = os.path.join(tests.get_test_workflow_directory(), "WorkflowTestVideo.json")
     wf = workflow.load(wf_path)
     wf.set_auto_save(True)
-
-    dir_path = tests.get_test_image_directory()
+    wf.set_cfg_entry("WholeVideo", str(int(True)))
+    dir_path = tests.get_test_video_directory()
     wf.set_directory_input(dir_path)
-    logger.info("Start workflow on image directory...")
     wf.run()
     logger.info("Workflow finished successfully")
 
 
-def test_run_image_common():
-    logger.info("===== Test::execute workflow on common image inputs =====")
+def test_run_on():
+    logger.info("===== Test: Workflow::run_on() =====")
+
+    # On array
+    logger.info("Run on image array...")
     wf_path = tests.get_test_workflow_directory() + "/WorkflowTest1.json"
     wf = workflow.load(wf_path)
-    wf.set_auto_save(True)
-
-    logger.info("----- Run on image from array")
     img_array = np.random.randint(low=0, high=255, size=(512, 512, 3), dtype=np.uint8)
     wf.run_on(array=img_array)
+    _check_leaf_outputs(wf)
+    logger.info("Workflow finished successfully")
 
-    logger.info("----- Run on image from file path")
+    # On image path
+    logger.info("Run on image path...")
+    wf_path = tests.get_test_workflow_directory() + "/WorkflowTest1.json"
+    wf = workflow.load(wf_path)
     img_path = tests.get_test_image_directory() + "/Lena.png"
     wf.run_on(path=img_path)
+    _check_leaf_outputs(wf)
+    logger.info("Workflow finished successfully")
 
-    logger.info("----- Run on image from URL")
+    # On image URL
+    logger.info("Run on image URL...")
+    wf_path = tests.get_test_workflow_directory() + "/WorkflowTest1.json"
+    wf = workflow.load(wf_path)
     img_url = "https://helpx.adobe.com/content/dam/help/en/photoshop/using/convert-color-image-black-white/jcr_content/main-pars/before_and_after/image-before/Landscape-Color.jpg"
     wf.run_on(url=img_url)
+    _check_leaf_outputs(wf)
+    logger.info("Workflow finished successfully")
 
-    logger.info("----- Run on image from folder")
-    dir_path = tests.get_test_image_directory()
-    wf.run_on(folder=dir_path)
-
-
-def test_run_single_video():
-    logger.info("===== Test::execute workflow on single video =====")
-    wf_path = tests.get_test_workflow_directory() + "/WorkflowTest1.json"
+    # On video path
+    logger.info("Run on video path...")
+    wf_path = tests.get_test_workflow_directory() + "/WorkflowTestVideo.json"
     wf = workflow.load(wf_path)
-    wf.set_auto_save(True)
-
-    logger.info("----- Run on video from file path")
-    video_path = tests.get_test_video_directory() + "/basketball.mp4"
+    video_path = os.path.join(tests.get_test_video_directory(), "basketball.mp4")
     wf.run_on(path=video_path)
+    _check_leaf_outputs(wf)
+    logger.info("Workflow finished successfully")
 
-    logger.info("----- Run on video from URL")
+    # On video URL
+    logger.info("Run on video URL...")
+    wf_path = tests.get_test_workflow_directory() + "/WorkflowTestVideo.json"
+    wf = workflow.load(wf_path)
     video_url = "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ForBiggerBlazes.mp4"
     wf.run_on(url=video_url)
+    _check_leaf_outputs(wf)
+    logger.info("Workflow finished successfully")
 
-
-def test_run_video_folder():
-    logger.info("===== Test::execute workflow on folder =====")
-    wf_path = tests.get_test_workflow_directory() + "/WorkflowTest1.json"
+    # On image folder
+    logger.info("Run on image folder...")
+    wf_path = tests.get_test_workflow_directory() + "/WorkflowTestVideo.json"
     wf = workflow.load(wf_path)
     wf.set_auto_save(True)
-
-    logger.info("----- Run on video from folder")
-    dir_path = tests.get_test_video_directory()
+    dir_path = tests.get_test_image_directory()
     wf.run_on(folder=dir_path)
+    _check_leaf_outputs(wf)
+    logger.info("Workflow finished successfully")
 
-
-def test_resnet_train(dataset_dir):
-    logger.info("===== Test::launch ResNet training =====")
-    wf_path = tests.get_test_workflow_directory() + "/WorkflowResNetTrain.json"
-    wf = workflow.load(wf_path)
-    logger.info("Start ResNet training...")
-    wf.run_on(folder=dataset_dir)
-    logger.info("Training finished successfully")
-
-
-def test_yolov5_train(wgisd_dataset_dir):
-    logger.info("===== Test::launch YoloV5 training =====")
-    wf_path = tests.get_test_workflow_directory() + "/WorkflowYoloV5Train.json"
+    # On video folder
+    logger.info("Run on video folder...")
+    wf_path = tests.get_test_workflow_directory() + "/WorkflowTestVideo.json"
     wf = workflow.load(wf_path)
+    wf.set_auto_save(True)
+    dir_path = tests.get_test_video_directory()
+    wf.run_on(folder=dir_path)
+    _check_leaf_outputs(wf)
+    logger.info("Workflow finished successfully")
 
-    # set dataset directory
-    wgisd_id, wgisd = wf.find_task(ik.dataset_wgisd)
-    wgisd_params = wgisd.get_parameters()
-    wgisd_params[ik.dataset_wgisd_param.data_folder_path] = wgisd_dataset_dir + "/data"
-    wgisd_params[ik.dataset_wgisd_param.class_file_path] = wgisd_dataset_dir + "/classes.txt"
-    wgisd.set_parameters(wgisd_params)
-
-    logger.info("Start YoloV5 training...")
-    wf.run()
-    logger.info("Training finished successfully")
-
-
-def test_yolo_train(wgisd_dataset_dir):
-    logger.info("===== Test::launch Darknet YOLO training =====")
-    wf = workflow.create("YoloTrain")
-    wgisd_id, wgisd = wf.add_task(ik.dataset_wgisd)
-    wgisd_params = wgisd.get_parameters()
-    wgisd_params[ik.dataset_wgisd_param.data_folder_path] = wgisd_dataset_dir + "/data"
-    wgisd_params[ik.dataset_wgisd_param.class_file_path] = wgisd_dataset_dir + "/classes.txt"
-    wgisd_params[ik.dataset_wgisd_param.seg_mask_mode] = "None"
-    wgisd.set_parameters(wgisd_params)
 
-    yolo_id, yolo = wf.add_task(ik.train_yolo)
-    yolo_params = yolo.get_parameters()
-    yolo_params["batchSize"] = 16
-    task.set_parameters(yolo, yolo_params)
-    wf.connect_tasks(wgisd_id, yolo_id)
+def test_add_task():
+    logger.info("===== Test: Workflow::add_task() =====")
 
-    wf.run()
+    # From name
+    wf = workflow.create("FromScratch")
+    box_filter = wf.add_task(name=ik.ocv_box_filter.name())
+    assert box_filter is not None
+    assert wf.get_task_count() == 2
+    assert len(wf.get_parents(wf.get_task_id(box_filter))) == 0
 
+    # From task object
+    wf = workflow.create("FromScratch")
+    box_filter = ikomia.ik_registry.create_algorithm(name=ik.ocv_box_filter.name())
+    box_filter_added = wf.add_task(task=box_filter)
+    assert box_filter_added == box_filter
+    assert wf.get_task_count() == 2
+    assert len(wf.get_parents(wf.get_task_id(box_filter))) == 0
 
-def test_export_graphviz():
-    logger.info("===== Test::export workflow as Graphviz =====")
+    # Default parameters
+    wf = workflow.create("FromScratch")
+    params = {
+        ik.ocv_box_filter.kSizeWidth: "19",
+        ik.ocv_box_filter.kSizeHeight: "19",
+    }
+    box_filter = wf.add_task(name=ik.ocv_box_filter.name(), params=params)
+    assert box_filter is not None
+    assert wf.get_task_count() == 2
+    assert len(wf.get_parents(wf.get_task_id(box_filter))) == 0
+    task_params = box_filter.get_parameters()
+    assert params[ik.ocv_box_filter.kSizeWidth] == task_params[ik.ocv_box_filter.kSizeWidth]
+    assert params[ik.ocv_box_filter.kSizeHeight] == task_params[ik.ocv_box_filter.kSizeHeight]
+
+    # Auto-connect
+    wf = workflow.create("FromScratch")
+    box_filter = wf.add_task(name=ik.ocv_box_filter.name(), auto_connect=True)
+    assert box_filter is not None
+    assert wf.get_task_count() == 2
+    parents = wf.get_parents(wf.get_task_id(box_filter))
+    assert len(parents) == 1
+    assert parents[0] == wf.get_task_id(wf.root())
+
+    canny = wf.add_task(name=ik.ocv_canny.name(), auto_connect=True)
+    assert canny is not None
+    assert wf.get_task_count() == 3
+    parents = wf.get_parents(wf.get_task_id(canny))
+    assert len(parents) == 1
+    assert parents[0] == wf.get_task_id(box_filter)
+
+
+def test_remove_task():
+    logger.info("===== Test: Workflow::remove_task() =====")
     wf_path = tests.get_test_workflow_directory() + "/WorkflowTest1.json"
     wf = workflow.load(wf_path)
-    dot_file_name = wf.name + ".dot"
-    path = os.path.join(config.main_cfg["data"]["path"], dot_file_name)
-    wf.export_graphviz(path)
-
-
-def test_graph_build():
-    logger.info("===== Test::create workflow from scratch =====")
-    wf = workflow.create("FromScratch")
-
-    # branch with auto-connection
-    box_filter_id, box_filter = wf.add_task(ik.ocv_box_filter)
-    wf.connect_tasks(wf.getRootID(), box_filter_id)
-
-    clahe_id, clahe = wf.add_task(ik.ocv_clahe)
-    wf.connect_tasks(box_filter_id, clahe_id)
-
-    dtfilterenhance_id, dtfilterenhance = wf.add_task(ik.ocv_dt_filter_enhance)
-    wf.connect_tasks(clahe_id, dtfilterenhance_id)
+    assert wf.get_task_count() == 10
 
-    lsc_id, lsc = wf.add_task(ik.ocv_superpixel_lsc)
-    wf.connect_tasks(dtfilterenhance_id, lsc_id)
+    # From name
+    wf.remove_task(name=ik.ocv_superpixel_lsc.name(), index=0)
+    assert wf.get_task_count() == 9
 
-    # branch with manual connection
-    bilateral_id, bilateral = wf.add_task(ik.ocv_bilateral_filter)
-    wf.connect_tasks(wf.getRootID(), bilateral_id, [(0, 0)])
+    # From task object
+    superpixel_seed = wf.find_task(name=ik.ocv_superpixel_seeds.name(), index=0)
+    wf.remove_task(task=superpixel_seed)
+    assert wf.get_task_count() == 8
 
-    equalize_id, equalize = wf.add_task(ik.ocv_equalize_histogram)
-    wf.connect_tasks(bilateral_id, equalize_id, [(0, 0)])
 
-    dtfilter_id, dtfilter = wf.add_task(ik.ocv_dt_filter)
-    wf.connect_tasks(equalize_id, dtfilter_id, [(0, 0), (0, 1)])
+def test_connect_tasks():
+    logger.info("===== Test: Workflow::connect_tasks() =====")
 
-    convert_id, convert = wf.add_task(ik.ocv_convert_to)
-    wf.connect_tasks(dtfilter_id, convert_id, [(0, 0)])
-
-    seeds_id, seeds = wf.add_task(ik.ocv_superpixel_seeds)
-    wf.connect_tasks(convert_id, seeds_id, [(0, 0)])
-
-    # visualization
-    displayIO.display(wf, "Manually built workflow")
-
-    # run
-    img_path = tests.get_test_image_directory() + "/Lena.png"
-    wf.set_image_input(path=img_path)
-    wf.run()
-
-    # check results
-    leaf_tasks = wf.get_final_tasks()
-    for t in leaf_tasks:
-        displayIO.display(t, t.name)
+    # Auto-connection
+    wf = workflow.create("FromScratch")
+    box_filter = wf.add_task(name=ik.ocv_box_filter.name(), auto_connect=False)
+    wf.connect_tasks(wf.root(), box_filter)
+    parents = wf.get_parents(wf.get_task_id(box_filter))
+    assert len(parents) == 1
+    assert parents[0] == wf.get_task_id(wf.root())
+
+    clahe = wf.add_task(name=ik.ocv_clahe.name(), auto_connect=False)
+    wf.connect_tasks(box_filter, clahe)
+    parents = wf.get_parents(wf.get_task_id(clahe))
+    assert len(parents) == 1
+    assert parents[0] == wf.get_task_id(box_filter)
+
+    # Manual connection
+    wf = workflow.create("FromScratch")
+    bilateral = wf.add_task(name=ik.ocv_bilateral_filter.name())
+    wf.connect_tasks(wf.root(), bilateral, [(0, 0)])
+    parents = wf.get_parents(wf.get_task_id(bilateral))
+    assert parents[0] == wf.get_task_id(wf.root())
+    edge_id = wf.get_in_edges(wf.get_task_id(bilateral))
+    assert len(edge_id) == 1
+    edge_info = wf.get_edge_info(edge_id[0])
+    assert edge_info[0] == 0
+    assert edge_info[1] == 0
+
+    equalize = wf.add_task(name=ik.ocv_equalize_histogram.name())
+    wf.connect_tasks(bilateral, equalize, [(0, 0)])
+    parents = wf.get_parents(wf.get_task_id(equalize))
+    assert parents[0] == wf.get_task_id(bilateral)
+    edge_id = wf.get_in_edges(wf.get_task_id(equalize))
+    assert len(edge_id) == 1
+    edge_info = wf.get_edge_info(edge_id[0])
+    assert edge_info[0] == 0
+    assert edge_info[1] == 0
+
+    dtfilter = wf.add_task(name=ik.ocv_dt_filter.name())
+    wf.connect_tasks(equalize, dtfilter, [(0, 0), (0, 1)])
+    parents = wf.get_parents(wf.get_task_id(dtfilter))
+    assert parents[0] == wf.get_task_id(equalize)
+    edge_id = wf.get_in_edges(wf.get_task_id(dtfilter))
+    assert len(edge_id) == 2
+    edge_info = wf.get_edge_info(edge_id[0])
+    assert edge_info[0] == 0
+    assert edge_info[1] == 0
+    edge_info = wf.get_edge_info(edge_id[1])
+    assert edge_info[0] == 0
+    assert edge_info[1] == 1
 
 
 def test_time_metrics():
     logger.info("===== Test::compute workflow time metrics =====")
-    img_path = tests.get_test_image_directory() + "/Lena.png"
-    wf_path = tests.get_test_workflow_directory() + "/WorkflowTest1.json"
+    img_path = os.path.join(tests.get_test_image_directory(), "Lena.png")
+    wf_path = os.path.join(tests.get_test_workflow_directory(), "WorkflowTest1.json")
     wf = workflow.load(wf_path)
     wf.set_image_input(path=img_path)
     wf.run()
@@ -388,136 +635,49 @@ def test_time_metrics():
     logger.info(wf.get_time_metrics())
 
 
-def test_get_outputs(wgisd_dataset_dir):
-    logger.info("===== Test::get workflow outputs of various data types =====")
-    img_path = tests.get_test_image_directory() + "/Lena.png"
-    wf_path = tests.get_test_workflow_directory() + "/WorkflowTestOutput.json"
+def test_get_task_output():
+    logger.info("===== Test: Workflow::get_task_output() =====")
+    img_path = os.path.join(tests.get_test_image_directory(), "Lena.png")
+    wf_path = os.path.join(tests.get_test_workflow_directory(), "WorkflowTestOutput.json")
     wf = workflow.load(wf_path)
-
-    # set WGISD_Dataset parameters
-    wgisd_task = wf.find_task(ik.dataset_wgisd, 0)[1]
-    wgisd_params = wgisd_task.get_parameters()
-    wgisd_params[ik.dataset_wgisd_param.data_folder_path] = wgisd_dataset_dir + "/data"
-    wgisd_params[ik.dataset_wgisd_param.class_file_path] = wgisd_dataset_dir + "/classes.txt"
-    wgisd_params[ik.dataset_wgisd_param.seg_mask_mode] = "None"
-    wgisd_task.set_parameters(wgisd_params)
-
-    # run workflow
-    wf.set_image_input(path=img_path)
-    wf.run()
-
-    logger.info("----- Get MobileNet SSD outputs: image, graphics and blob measure")
-    img_out = wf.get_image_output(task_name=ik.infer_mobilenet_ssd)
-    assert (img_out is not None)
-    displayIO.display(img_out, "MobileNet SSD")
-    graphics_out = wf.get_graphics_output(task_name=ik.infer_mobilenet_ssd)
-    assert (graphics_out is not None)
-    displayIO.display(graphics_out, "MobileNet SSD")
-    blob_out = wf.get_blob_measure_output(task_name=ik.infer_mobilenet_ssd)
-    assert (blob_out is not None)
-    displayIO.display(blob_out, "MobileNet SSD")
-
-    logger.info("----- Get Split Operator outputs: 3 images")
-    img_out = wf.get_image_output(task_name=ik.ocv_split)
-    assert (img_out is not None)
-    assert (len(img_out) == 3)
-    img_out = wf.get_image_output(task_name=ik.ocv_split, index=2)
-    assert (img_out is not None)
-    displayIO.display(img_out, "Blue channel")
-
-    logger.info("----- Get CalcHist outputs: numeric")
-    hist_task_id = wf.find_task(ik.ocv_calc_hist, 0)[0]
-    numeric_out = wf.get_numeric_output(task_id=hist_task_id)
-    assert (numeric_out is not None)
-    displayIO.display(numeric_out, "CalcHist")
-
-    logger.info("----- Get WGISD_Dataset outputs: dataset")
-    dataset_out = wf.get_dataset_output(task_name=ik.dataset_wgisd)
-    assert (dataset_out is not None)
-    assert (dataset_out.getCategoryCount() > 0)
-    assert (len(dataset_out.getImagePaths()) > 0)
-
-
-def test_get_image_with_graphics():
-    logger.info("===== Test::get image with graphics =====")
-    # create workflow with MobileNet SSD task
-    img_path = tests.get_test_image_directory() + "/Lena.png"
-    wf = workflow.create("ImageWithGraphics")
-    id1, t1 = wf.add_task(ik.infer_mobilenet_ssd)
-    wf.connect_tasks(wf.getRootID(), id1)
     wf.run_on(path=img_path)
 
-    logger.info("-----Get image from task id")
-    image1 = wf.get_image_with_graphics(task_id=id1, image_index=0, graphics_index=0)
-    assert(image1 is not None)
-    cv2.imshow("MobileNet SSD", image1)
-    cv2.waitKey(0)
-
-    # add second MobileNet SSD task
-    id2, t2 = wf.add_task(ik.infer_mobilenet_ssd)
-    wf.connect_tasks(wf.getRootID(), id2)
-    wf.run_on(path=img_path)
+    outs = wf.get_task_output(task_name=ik.ocv_split.name(), task_index=0, types=[IODataType.IMAGE], output_index=-1)
+    assert len(outs) == 3
+    outs = wf.get_task_output(task_name=ik.ocv_split.name(), types=[IODataType.IMAGE], output_index=0)
+    assert not isinstance(outs, list)
+    assert isinstance(outs, CImageIO)
 
-    logger.info("-----Get image from task name")
-    images = wf.get_image_with_graphics(task_name=ik.infer_mobilenet_ssd, image_index=0, graphics_index=0)
-    assert(len(images) == 2)
-    cv2.imshow("MobileNet SSD #1", images[0])
-    cv2.imshow("MobileNet SSD #2", images[1])
-    cv2.waitKey(0)
+    split = wf.find_task(name=ik.ocv_split.name(), index=0)
+    outs = wf.get_task_output(task_obj=split, task_index=0, types=[IODataType.IMAGE], output_index=-1)
+    assert len(outs) == 3
+    outs = wf.get_task_output(task_obj=split, types=[IODataType.IMAGE], output_index=0)
+    assert not isinstance(outs, list)
+    assert isinstance(outs, CImageIO)
 
+    outs = wf.get_task_output(task_name=ik.ocv_calc_hist.name(), types=[IODataType.NUMERIC_VALUES], output_index=0)
+    assert isinstance(outs, CNumericIO)
 
-def test_get_image():
-    logger.info("===== Test::get image =====")
-    img_path = tests.get_test_image_directory() + "/Lena.png"
-    wf_path = tests.get_test_workflow_directory() + "/WorkflowTest1.json"
-    wf = workflow.load(wf_path)
-    wf.run_on(path=img_path)
-
-    image = wf.get_image(task_name=ik.ocv_box_filter)
-    assert image.size != 0
-    clahe_id, clahe = wf.find_task(name=ik.ocv_clahe)
-    image = wf.get_image(task_id=clahe_id)
-    assert image.size != 0
-    images = wf.get_image(task_name=ik.ocv_superpixel_lsc)
-    assert(len(images) == 3)
-    image = wf.get_image(task_name=ik.ocv_superpixel_lsc, index=0)
-    assert image.size != 0
+    outs = wf.get_task_output(task_name=ik.infer_mobilenet_ssd.name(), types=[IODataType.OBJECT_DETECTION], output_index=0)
+    assert isinstance(outs, CObjectDetectionIO)
 
-
-def test_set_task_parameters():
-    logger.info("===== Test::set parameters of specific tasks =====")
-    wf_path = tests.get_test_workflow_directory() + "/WorkflowTest1.json"
-    wf = workflow.load(wf_path)
-
-    bf_id, box_filter = wf.find_task(name=ik.ocv_box_filter, index=0)
-    logger.info(box_filter.get_param_object())
-    wf.set_parameters({ik.ocv_box_filter_param.kSizeHeight: 11,
-                       ik.ocv_box_filter_param.kSizeWidth: 11}, task_name=ik.ocv_box_filter, index=0)
-    params = box_filter.get_parameters()
-    assert (params["kSizeHeight"] == str(11) and params["kSizeWidth"] == str(11))
-
-    bl_id, bilateral_filter = wf.find_task(name=ik.ocv_bilateral_filter, index=0)
-    logger.info(bilateral_filter.get_param_object())
-    wf.set_parameters({ik.ocv_bilateral_filter_param.sigmaSpace: 31.0,
-                       ik.ocv_bilateral_filter_param.sigmaColor: 11.0}, task_id=bl_id)
-    params = bilateral_filter.get_parameters()
-
-    assert (float(params["sigmaSpace"]) == 31.0 and float(params["sigmaColor"]) == 11.0)
+    outs = wf.get_task_output(task_name=ik.infer_mask_rcnn.name(), types=[IODataType.INSTANCE_SEGMENTATION], output_index=0)
+    assert isinstance(outs, CInstanceSegmentationIO)
 
 
 def test_video_stream():
     cap = cv2.VideoCapture(0)
 
     wf = workflow.create("Video Processing")
-    clahe_id, clahe = wf.add_task(ik.ocv_clahe)
-    wf.connect_tasks(wf.getRootID(), clahe_id)
-    canny_id, canny = wf.add_task(ik.ocv_canny)
-    wf.connect_tasks(clahe_id, canny_id)
+    clahe = wf.add_task(ik.ocv_clahe())
+    wf.connect_tasks(wf.root(), clahe)
+    canny = wf.add_task(ik.ocv_canny())
+    wf.connect_tasks(clahe, canny)
 
     while True:
         ret, img = cap.read()
         wf.run_on(array=img)
-        res_img = wf.get_image(canny_id, 0)
+        res_img = wf.get_image(wf.get_task_id(canny), 0)
         cv2.imshow("Result", res_img)
 
         if cv2.waitKey(1) == 27:
@@ -541,43 +701,35 @@ def test_video_stream():
     opt = parser.parse_args()
     running_tests = opt.tests.split(',')
 
-    ikomia.authenticate()
-
     if 'all' in running_tests or 'cpp_workflow' in running_tests:
         test_cpp_workflow()
-    if 'all' in running_tests or 'builtin' in running_tests:
-        test_load_builtin()
-    if 'all' in running_tests or 'marketplace' in running_tests:
-        test_load_marketplace()
-    if 'all' in running_tests or 'run_single_image' in running_tests:
-        test_run_single_image()
-    if 'all' in running_tests or 'run_image_folder' in running_tests:
-        test_run_image_folder()
-    if 'all' in running_tests or 'run_image_common' in running_tests:
-        test_run_image_common()
-    if 'all' in running_tests or 'run_single_video' in running_tests:
-        test_run_single_video()
-    if 'all' in running_tests or 'run_video_folder' in running_tests:
-        test_run_video_folder()
-    if 'all' in running_tests or 'graphviz' in running_tests:
-        test_export_graphviz()
+    if 'all' in running_tests or 'ctor' in running_tests:
+        test_ctor()
+    if 'all' in running_tests or 'root' in running_tests:
+        test_root()
+    if 'all' in running_tests or 'set_image_input' in running_tests:
+        test_set_image_input()
+    if 'all' in running_tests or 'set_video_input' in running_tests:
+        test_set_video_input()
+    if 'all' in running_tests or 'set_directory_input' in running_tests:
+        test_set_directory_input()
+    if 'all' in running_tests or 'set_parameters' in running_tests:
+        test_set_parameters()
+    if 'all' in running_tests or 'load' in running_tests:
+        test_load()
+    if 'all' in running_tests or 'run' in running_tests:
+        test_run()
+    if 'all' in running_tests or 'run_on' in running_tests:
+        test_run_on()
+    if 'all' in running_tests or 'add_task' in running_tests:
+        test_add_task()
+    if 'all' in running_tests or 'remove_task' in running_tests:
+        test_remove_task()
+    if 'all' in running_tests or 'connect_tasks' in running_tests:
+        test_connect_tasks()
     if 'all' in running_tests or 'time_metrics' in running_tests:
         test_time_metrics()
-    if 'all' in running_tests or 'graph_build' in running_tests:
-        test_graph_build()
-    if 'all' in running_tests or 'outputs' in running_tests:
-        test_get_outputs(opt.detect_dataset_dir)
-    if 'all' in running_tests or 'output_image_graphics' in running_tests:
-        test_get_image_with_graphics()
-    if 'all' in running_tests or 'output_image' in running_tests:
-        test_get_image()
-    if 'all' in running_tests or 'set_parameters' in running_tests:
-        test_set_task_parameters()
+    if 'all' in running_tests or 'get_task_output' in running_tests:
+        test_get_task_output()
     if 'all' in running_tests or 'test_video_stream' in running_tests:
         test_video_stream()
-    if 'all' in running_tests or 'train_resnet' in running_tests:
-        test_resnet_train(opt.classif_dataset_dir)
-    if 'all' in running_tests or 'train_yolov4' in running_tests:
-        test_yolo_train(opt.detect_dataset_dir)
-    if 'all' in running_tests or 'train_yolov5' in running_tests:
-        test_yolov5_train(opt.detect_dataset_dir)