Repository: NVIDIA/fsi-samples (language: python)

Issue:
Synch master with develop




Discussion / hints:

Fix patch:
diff --git a/gQuant/docker/build.sh b/gQuant/docker/build.sh
index e9c1618c..2f45f5ea 100755
--- a/gQuant/docker/build.sh
+++ b/gQuant/docker/build.sh
@@ -39,7 +39,7 @@ echo -e "\nPlease, select your CUDA version:\n" \
 
 read -p "Enter your option and hit return [1]-3: " CUDA_VERSION
 
-RAPIDS_VERSION="0.17.0"
+RAPIDS_VERSION="0.19.0"
 
 CUDA_VERSION=${CUDA_VERSION:-1}
 case $CUDA_VERSION in
diff --git a/gQuant/plugins/dask_plugin/README.md b/gQuant/plugins/dask_plugin/README.md
new file mode 100644
index 00000000..7bb1deaa
--- /dev/null
+++ b/gQuant/plugins/dask_plugin/README.md
@@ -0,0 +1,31 @@
+## Greenflow Dask Plugin 
+
+This is a Greenflow plugin that includes a set of utlility nodes for Dask workflows. 
+
+Greenflow take advantage of the `entry point` inside the `setup.py` file to register the plugin. Greenflow can discover all the plugins that has the entry point group name `greenflow.plugin`. Check the `setup.py` file to see details.
+
+### Create an new Python enviroment
+```bash
+conda create -n test python=3.8
+```
+
+### Install the greenflowlab JupyterLab plugin
+To install `greenflowlab` JupyterLab plugin, make sure `nodejs` of version [12^14^15] is installed. E.g:
+```bash
+conda install -c conda-forge python-graphviz nodejs=12.4.0 pydot
+```
+Then install the `greenflowlab`:
+```bash
+pip install greenflowlab
+```
+Or install `greenflowlab` at the greenflowlab directory:
+```bash
+pip install .
+```
+
+### Install the external example plugin
+To install the external plugin, in the plugin diretory, run following command
+```bash
+pip install .
+```
+
diff --git a/gQuant/plugins/dask_plugin/greenflow_dask_plugin/__init__.py b/gQuant/plugins/dask_plugin/greenflow_dask_plugin/__init__.py
new file mode 100644
index 00000000..044aaeba
--- /dev/null
+++ b/gQuant/plugins/dask_plugin/greenflow_dask_plugin/__init__.py
@@ -0,0 +1,5 @@
+from .daskComputeNode import DaskComputeNode
+from .persistNode import PersistNode
+from .simpleParallelNode import SimpleParallelNode
+
+__all__ = ["DaskComputeNode", "PersistNode", "SimpleParallelNode"]
diff --git a/gQuant/plugins/dask_plugin/greenflow_dask_plugin/daskComputeNode.py b/gQuant/plugins/dask_plugin/greenflow_dask_plugin/daskComputeNode.py
new file mode 100644
index 00000000..91041b4e
--- /dev/null
+++ b/gQuant/plugins/dask_plugin/greenflow_dask_plugin/daskComputeNode.py
@@ -0,0 +1,72 @@
+from greenflow.dataframe_flow import (ConfSchema, PortsSpecSchema)
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from greenflow.dataframe_flow import Node
+from dask.dataframe import DataFrame as DaskDataFrame
+import dask.distributed
+
+
+class DaskComputeNode(TemplateNodeMixin, Node):
+
+    def init(self):
+        TemplateNodeMixin.init(self)
+        port_type = PortsSpecSchema.port_type
+        dy = PortsSpecSchema.dynamic
+        self.INPUT_PORT_NAME = 'in'
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame",
+                    "builtins.object"
+                ],
+                dy: {
+                    PortsSpecSchema.DYN_MATCH: ["cudf.DataFrame"]
+                }
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=None
+        )
+
+    def conf_schema(self):
+        json = {
+            "title": "Compute the dask dataframe",
+            "type": "object",
+            "properties": {
+            },
+        }
+
+        ui = {}
+        return ConfSchema(json=json, ui=ui)
+
+    def process(self, inputs):
+        # df = df.drop('datetime', axis=1)
+        input_connections = self.get_connected_inports()
+        determined_type = None
+        for port_name in input_connections.keys():
+            if port_name != self.INPUT_PORT_NAME:
+                determined_type = input_connections[port_name]
+
+        output = {}
+        if (determined_type[0] is not None and
+                issubclass(determined_type[0], DaskDataFrame)):
+            client = dask.distributed.client.default_client()
+
+            objs = []
+            for port_name in input_connections.keys():
+                if port_name != self.INPUT_PORT_NAME:
+                    df = inputs[port_name]
+                    objs.append(df)
+
+            objs = client.compute(objs)
+            for port_name in input_connections.keys():
+                if port_name != self.INPUT_PORT_NAME:
+                    output[port_name] = objs.pop(0).result()
+
+        else:
+            for port_name in input_connections.keys():
+                if port_name != self.INPUT_PORT_NAME:
+                    df = inputs[port_name]
+                    output[port_name] = df
+
+        return output
diff --git a/gQuant/plugins/dask_plugin/greenflow_dask_plugin/persistNode.py b/gQuant/plugins/dask_plugin/greenflow_dask_plugin/persistNode.py
new file mode 100644
index 00000000..f632ce3e
--- /dev/null
+++ b/gQuant/plugins/dask_plugin/greenflow_dask_plugin/persistNode.py
@@ -0,0 +1,69 @@
+from greenflow.dataframe_flow import (ConfSchema, PortsSpecSchema)
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from greenflow.dataframe_flow import Node
+from dask.dataframe import DataFrame as DaskDataFrame
+import dask.distributed
+
+
+class PersistNode(TemplateNodeMixin, Node):
+
+    def init(self):
+        TemplateNodeMixin.init(self)
+        port_type = PortsSpecSchema.port_type
+        dy = PortsSpecSchema.dynamic
+        self.INPUT_PORT_NAME = 'in'
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame",
+                    "builtins.object"
+                ],
+                dy: {
+                    PortsSpecSchema.DYN_MATCH: True
+                }
+            },
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=None
+        )
+
+    def conf_schema(self):
+        json = {
+            "title": "Persist the dask dataframe",
+            "type": "object",
+            "properties": {
+            },
+        }
+
+        ui = {}
+        return ConfSchema(json=json, ui=ui)
+
+    def process(self, inputs):
+        # df = df.drop('datetime', axis=1)
+        input_connections = self.get_connected_inports()
+        determined_type = None
+        for port_name in input_connections.keys():
+            if port_name != self.INPUT_PORT_NAME:
+                determined_type = input_connections[port_name]
+
+        output = {}
+        if (determined_type[0] is not None and issubclass(determined_type[0],
+                                                          DaskDataFrame)):
+            client = dask.distributed.client.default_client()
+            input_connections = self.get_connected_inports()
+            objs = []
+            for port_name in input_connections.keys():
+                if port_name != self.INPUT_PORT_NAME:
+                    df = inputs[port_name]
+                    objs.append(df)
+            objs = client.persist(objs)
+            for port_name in input_connections.keys():
+                if port_name != self.INPUT_PORT_NAME:
+                    output[port_name] = objs.pop(0)
+        else:
+            for port_name in input_connections.keys():
+                if port_name != self.INPUT_PORT_NAME:
+                    df = inputs[port_name]
+                    output[port_name] = df
+        return output
diff --git a/gQuant/plugins/dask_plugin/greenflow_dask_plugin/simpleParallelNode.py b/gQuant/plugins/dask_plugin/greenflow_dask_plugin/simpleParallelNode.py
new file mode 100644
index 00000000..ca029e13
--- /dev/null
+++ b/gQuant/plugins/dask_plugin/greenflow_dask_plugin/simpleParallelNode.py
@@ -0,0 +1,242 @@
+from greenflow.plugin_nodes import CompositeNode
+from greenflow.plugin_nodes import ContextCompositeNode
+from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema,
+                                                      PortsSpecSchema,
+                                                      NodePorts)
+from greenflow.plugin_nodes.util.json_util import parse_config
+from dask.dataframe import DataFrame as DaskDataFrame
+import dask
+from jsonpath_ng import parse
+import dask.distributed
+
+__all__ = ["SimpleParallelNode"]
+
+
+class SimpleParallelNode(CompositeNode):
+    """
+    A SimpleParallelNode that can take a single GPU/CPU workflow, and convert
+    it to output Dask dataframe. Each partition in the Dask dataframe will be
+    computed in parallel in different GPU/CPUs.
+
+    In the SimpleParallelNode configuration, user just need to set the
+    taskgraph, the inputs and outputs of the taskgraph,
+    the context parameters for the taskgraph. Most importantly,
+    it can map the iteration id (or the Dask Dataframe partition id) to any
+     number typed configuration item of the taskgraph.
+     E.g. it can be used to set the randomn seed number for each of the
+     iteration runs.
+    """
+
+    def init(self):
+        super().init()
+
+    def ports_setup(self):
+        ports = super().ports_setup()
+        port_type = PortsSpecSchema.port_type
+        inports = ports.inports
+        outports = ports.outports
+        for k in outports.keys():
+            outports[k][port_type] = [DaskDataFrame]
+        output_port = NodePorts(inports=inports, outports=outports)
+        return output_port
+
+    def meta_setup(self):
+        out_meta = super().meta_setup()
+        return out_meta
+
+    def conf_schema(self):
+        task_graph = self.task_graph
+        replacementObj = self.replacementObj
+        # cache_key, task_graph, replacementObj = self._compute_has
+        # cache_key, task_graph, replacementObj = self._compute_hash_key()
+        # if cache_key in CACHE_SCHEMA:
+        #     return CACHE_SCHEMA[cache_key]
+        conf = ContextCompositeNode.conf_schema(self)
+        json = {
+            "title": "Simple Parallel Node",
+            "type": "object",
+            "description": """The SimpleParallelNode is used to parallelize the
+             embarrassingly parallelizable cudf computation taskgraph.""",
+            "properties": {
+                "taskgraph": {
+                    "type": "string",
+                    "description": "the taskgraph filepath"
+                },
+                "input": {
+                    "type": "array",
+                    "description": "the input node ids",
+                    "items": {
+                        "type": "string"
+                    }
+                },
+                "output": {
+                    "type": "array",
+                    "description": "the output node ids",
+                    "items": {
+                        "type": "string"
+                    },
+                },
+                "iterations": {
+                    "type": "integer",
+                    "description": "the number of iterations",
+                    "items": {
+                        "type": "string"
+                    }
+                },
+            },
+            "required": ["taskgraph"]
+        }
+        ui = {
+            "taskgraph": {"ui:widget": "TaskgraphSelector"},
+        }
+
+        types = []
+        if 'taskgraph' in self.conf:
+            if 'context' in conf.json['properties']:
+                json['properties']['context'] = conf.json['properties'][
+                    'context']
+            json['properties']['map'] = {
+                "type": "array",
+                "description": """The iteration number maps to""",
+                "items": {
+                    "type": "object",
+                    "properties": {
+                        "node_id": {
+                            "type": "string",
+                            "enum": []
+                        }
+                    },
+                    "dependencies": {
+                        "node_id": {
+                            "oneOf": [],
+                        }
+                    }
+                }
+            }
+            all_fields = parse_config(replacementObj)
+            types = list(all_fields.keys())
+        if 'number' in types:
+            ty = 'number'
+            type_container = all_fields[ty]
+            ids = list(type_container.keys())
+            json['properties']['map']['items']['properties']['node_id'][
+                'enum'] = ids
+            idlist = json['properties']['map']['items']['dependencies'][
+                'node_id']['oneOf']
+            for subid in ids:
+                id_obj = {
+                    "properties": {
+                        "node_id": {
+                            "type": "string"
+                        },
+                        "xpath": {
+                            "type": "string",
+                        }
+                    }
+                }
+                content = type_container[subid]
+                paths = [i['path'] for i in content]
+                names = [i['item'] for i in content]
+                id_obj['properties']['node_id']['enum'] = [subid]
+                id_obj['properties']['xpath']['enum'] = paths
+                id_obj['properties']['xpath']['enumNames'] = names
+                idlist.append(id_obj)
+
+        pandas_df_name = 'pandas.core.frame.DataFrame'
+        cudf_df_name = 'cudf.core.dataframe.DataFrame'
+
+        if 'taskgraph' in self.conf:
+
+            def inputNode_fun(inputNode, in_ports):
+                pass
+
+            def outNode_fun(outNode, out_ports):
+                pass
+
+            self._make_sub_graph_connection(task_graph,
+                                            inputNode_fun, outNode_fun)
+
+            ids_in_graph = []
+            in_ports = []
+            out_ports = []
+            for t in task_graph:
+                node_id = t.get('id')
+                if node_id != '':
+                    node = task_graph[node_id]
+                    all_ports = node.ports_setup()
+                    for port in all_ports.inports.keys():
+                        in_ports.append(node_id+'.'+port)
+                    for port in all_ports.outports.keys():
+                        types = all_ports.outports[port][
+                            PortsSpecSchema.port_type]
+                        correct_type = False
+                        if isinstance(types, list):
+                            t_names = [
+                                t.__module__ + '.' + t.__name__ for t in types
+                            ]
+                            if (pandas_df_name in t_names
+                                    or cudf_df_name in t_names):
+                                correct_type = True
+                        else:
+                            t_names = types.__module__ + '.' + types.__name__
+                            if (pandas_df_name == t_names
+                                    or cudf_df_name == t_names):
+                                correct_type = True
+                        if correct_type:
+                            out_ports.append(node_id+'.'+port)
+                    ids_in_graph.append(node_id)
+            json['properties']['input']['items']['enum'] = in_ports
+            json['properties']['output']['items']['enum'] = out_ports
+        out_schema = ConfSchema(json=json, ui=ui)
+        # CACHE_SCHEMA[cache_key] = out_schema
+        return out_schema
+
+    def conf_update(self):
+        """
+        run after init, used to update configuration
+        """
+        pass
+
+    def update_replace(self, replaceObj, task_graph, **kwargs):
+        """
+        this method is called in the update
+
+        @para replaceObj is a dictionary of the configuration
+        @para task_graph is the task_graph loaded for this composite node
+        @iternum integer, the iteration number
+
+        It is intented to construct a new python dictionary to pass to the
+        task_graph.run replace argument. So the composite taskgraph can run
+         with different configurations.
+        """
+        ContextCompositeNode.update_replace(self, replaceObj, task_graph,
+                                            **kwargs)
+        # replace the numbers from the context
+        if 'map' in self.conf and 'iternum' in kwargs:
+            for i in range(len(self.conf['map'])):
+                val = kwargs['iternum']
+                map_obj = self.conf['map'][i]
+                xpath = map_obj['xpath']
+                expr = parse(xpath)
+                expr.update(replaceObj, val)
+
+    def process(self, inputs, **kwargs):
+        output = {}
+        # more_output = self._process(inputs)
+        # output.update(more_output)
+        iterations = self.conf['iterations']
+        out_dfs = [
+            dask.delayed(CompositeNode.process)(self, inputs, iternum=i)
+            for i in range(iterations)
+        ]
+        client = dask.distributed.client.default_client()
+        out_dfs = client.persist(out_dfs)
+        meta = self.meta_setup().outports
+        ports = self.ports_setup()
+        for name in ports.outports.keys():
+            if self.outport_connected(name):
+                meta_data = meta[name]
+                objs = [i[name] for i in out_dfs]
+                dask_df = dask.dataframe.from_delayed(objs, meta=meta_data)
+                output[name] = dask_df
+        return output
diff --git a/gQuant/plugins/dask_plugin/setup.py b/gQuant/plugins/dask_plugin/setup.py
new file mode 100644
index 00000000..6b8087a6
--- /dev/null
+++ b/gQuant/plugins/dask_plugin/setup.py
@@ -0,0 +1,15 @@
+from setuptools import setup, find_packages
+
+setup(
+    name='greenflow_dask_plugin',
+    version='0.0.1',
+    packages=find_packages(include=['greenflow_dask_plugin']),
+    install_requires=[
+        "greenflow"
+    ],
+    entry_points={
+        'greenflow.plugin': [
+            'greenflow_dask_plugin = greenflow_dask_plugin',
+        ],
+    }
+)
diff --git a/gQuant/plugins/gquant_plugin/README.md b/gQuant/plugins/gquant_plugin/README.md
index 6c977fc4..fface509 100644
--- a/gQuant/plugins/gquant_plugin/README.md
+++ b/gQuant/plugins/gquant_plugin/README.md
@@ -1,14 +1,14 @@
-## greenflow RAPIDS Plugin Example
-This is a example to show how to write an external greenflow RAPIDS plugin. greenflow take advantage of the `entry point` inside the `setup.py` file to register the plugin. greenflow can discover all the plugins that has the entry point group name `greenflow.plugin`. Check the `setup.py` file to see details.
-
+## Greenflow RAPIDS Plugin Example
 The examples range from simple accelerated calculation of technical trading indicators through defining workflows for interactively developing trading strategies and automating many typical tasks.
 
-The extensibility of the system is highlighted by examples showing how to create a dataframe flow graph, which allows for easy re-use and composability of higher level workflows.
+The extensibility of the system is highlighted by examples showing how to create a taskgraph workflow, which allows for easy re-use and composability of higher level workflows.
 
 The examples also show how to easily convert a single-threaded solution into a Dask distributed one. 
 
 These examples can be used as-is or, as they are open source, can be extended to suit your environments.
 
+Greenflow take advantage of the `entry point` inside the `setup.py` file to register the plugin. Greenflow can discover all the plugins that has the entry point group name `greenflow.plugin`. Check the `setup.py` file to see details.
+
 ### Create an new Python enviroment
 ```bash
 conda create -n test python=3.8
@@ -29,20 +29,10 @@ bash download_data.sh
 
 ```
 
-### Install the greenflow 
-To install the greenflow graph computation library, run:
-```bash
-pip install greenflow
-```
-Or install `greenflow` at the greenflow directory:
-```bash
-pip install .
-```
-
 ### Install the greenflowlab JupyterLab plugin
 To install `greenflowlab` JupyterLab plugin, make sure `nodejs` of version [12^14^15] is installed. E.g:
 ```bash
-conda install -c conda-forge nodejs=12.4.0
+conda install -c conda-forge python-graphviz nodejs=12.4.0 pydot
 ```
 Then install the `greenflowlab`:
 ```bash
@@ -56,7 +46,7 @@ pip install .
 ### Install the external example plugin
 Install RAPIDS:
 ```bash
-conda install -y -c rapidsai -c nvidia -c conda-forge -c defaults rapids=0.17
+conda install -y -c rapidsai -c nvidia -c conda-forge -c defaults rapids=0.19
 ```
 To install the external plugin, in the plugin diretory, run following command
 ```bash
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/__init__.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/__init__.py
index 840e3bdd..a2c9b76f 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/__init__.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/__init__.py
@@ -33,11 +33,12 @@ def _validate_df(df_to_val, ref_cols, obj):
 
     i_cols = df_to_val.columns
     if len(i_cols) != len(ref_cols):
-        print("expect %d columns, only see %d columns"
-              % (len(ref_cols), len(i_cols)))
-        print("ref:", ref_cols)
-        print("columns", i_cols)
-        raise Exception("not valid for node %s" % (obj.uid))
+        errmsg = 'Invalid for node "{:s}"\n'\
+            'Expect {:d} columns, only see {:d} columns\n'\
+            'Ref: {}\n'\
+            'Columns: {}'\
+            .format(obj.uid, len(ref_cols), len(i_cols), ref_cols, i_cols)
+        raise Exception(errmsg)
 
     for col in ref_cols.keys():
         if col not in i_cols:
@@ -72,6 +73,7 @@ def _validate_df(df_to_val, ref_cols, obj):
             # Maybe raise an exception here and have the caller
             # try/except the validation routine.
             return False
+
     return True
 
 
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/_port_type_node.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/_port_type_node.py
deleted file mode 100644
index 82206368..00000000
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/_port_type_node.py
+++ /dev/null
@@ -1,207 +0,0 @@
-from greenflow.dataframe_flow.portsSpecSchema import (PortsSpecSchema,
-                                                   MetaData,
-                                                   NodePorts)
-import cudf
-import dask_cudf
-import os
-import warnings
-import pandas as pd
-
-
-class _PortTypesMixin(object):
-
-    def init(self):
-        self.INPUT_PORT_NAME = 'in'
-        self.OUTPUT_PORT_NAME = 'out'
-
-    def ports_setup_different_output_type(self, out_type):
-        types = [cudf.DataFrame,
-                 dask_cudf.DataFrame,
-                 pd.DataFrame]
-        port_type = PortsSpecSchema.port_type
-        input_ports = {
-            self.INPUT_PORT_NAME: {
-                port_type: types
-            }
-        }
-        output_ports = {
-            self.OUTPUT_PORT_NAME: {
-                port_type: out_type
-            }
-        }
-
-        input_connections = self.get_connected_inports()
-        if (self.INPUT_PORT_NAME in input_connections):
-            determined_type = input_connections[self.INPUT_PORT_NAME]
-            # connected
-            return NodePorts(inports={self.INPUT_PORT_NAME: {
-                port_type: determined_type}},
-                outports=output_ports)
-        else:
-            return NodePorts(inports=input_ports, outports=output_ports)
-
-    def ports_setup_from_types(self, types):
-        port_type = PortsSpecSchema.port_type
-        input_ports = {
-            self.INPUT_PORT_NAME: {
-                port_type: types
-            }
-        }
-
-        output_ports = {
-            self.OUTPUT_PORT_NAME: {
-                port_type: types
-            }
-        }
-
-        input_connections = self.get_connected_inports()
-        if self.INPUT_PORT_NAME in input_connections:
-            determined_type = input_connections[self.INPUT_PORT_NAME]
-            # connected
-            return NodePorts(inports={self.INPUT_PORT_NAME: {
-                port_type: determined_type}},
-                outports={self.OUTPUT_PORT_NAME: {
-                    port_type: determined_type}})
-        else:
-            return NodePorts(inports=input_ports, outports=output_ports)
-
-    def ports_setup(self):
-        types = [cudf.DataFrame,
-                 dask_cudf.DataFrame,
-                 pd.DataFrame]
-        return self.ports_setup_from_types(types)
-
-    def meta_setup(self, required={}):
-        input_meta = self.get_input_meta()
-        if self.INPUT_PORT_NAME in input_meta:
-            col_from_inport = input_meta[self.INPUT_PORT_NAME]
-            output_cols = {
-                self.OUTPUT_PORT_NAME: col_from_inport
-            }
-            input_cols = {
-                self.INPUT_PORT_NAME: required
-            }
-            meta_data = MetaData(inports=input_cols, outports=output_cols)
-            return meta_data
-        else:
-            input_cols = {
-                self.INPUT_PORT_NAME: required
-            }
-            output_cols = {
-                self.OUTPUT_PORT_NAME: required
-            }
-            meta_data = MetaData(inports=input_cols, outports=output_cols)
-            return meta_data
-
-    def addition_meta_setup(self, addition, required={}):
-        input_meta = self.get_input_meta()
-        if self.INPUT_PORT_NAME not in input_meta:
-            col_from_inport = required
-        else:
-            col_from_inport = input_meta[self.INPUT_PORT_NAME]
-        input_cols = {
-            self.INPUT_PORT_NAME: required
-        }
-        # additional ports
-        output_cols = {
-            self.OUTPUT_PORT_NAME: col_from_inport
-        }
-        output_cols[self.OUTPUT_PORT_NAME].update(addition)
-        meta_data = MetaData(inports=input_cols, outports=output_cols)
-        return meta_data
-
-    def deletion_meta_setup(self, deletion, required={}):
-        input_meta = self.get_input_meta()
-        if self.INPUT_PORT_NAME not in input_meta:
-            col_from_inport = required
-        else:
-            col_from_inport = input_meta[self.INPUT_PORT_NAME]
-        # delete the columns from the inputs
-        input_cols = {
-            self.INPUT_PORT_NAME: required
-        }
-        for key in deletion:
-            if key in col_from_inport:
-                del col_from_inport[key]
-        meta_data = MetaData(inports=input_cols,
-                             outports={self.OUTPUT_PORT_NAME: col_from_inport})
-        return meta_data
-
-    def retention_meta_setup(self, retention, required={}):
-        input_cols = {
-            self.INPUT_PORT_NAME: required
-        }
-        meta_data = MetaData(inports=input_cols,
-                             outports={self.OUTPUT_PORT_NAME: retention})
-        return meta_data
-
-    def load_cache(self, filename=None) -> dict:
-        """
-        Defines the behavior of how to load the cache file from the `filename`.
-        Node can override this method. Default implementation assumes cudf
-        dataframes.
-
-        Arguments
-        -------
-        filename: str
-            filename of the cache file. Leave as none to use default.
-        returns: dict
-            dictionary of the output from this node
-        """
-        cache_dir = os.getenv('GREENFLOW_CACHE_DIR', self.cache_dir)
-        if filename is None:
-            filename = cache_dir + '/' + self.uid + '.hdf5'
-
-        output_df = {}
-        with pd.HDFStore(filename, mode='r') as hf:
-            for oport, pspec in \
-                    self._get_output_ports(full_port_spec=True).items():
-                ptype = pspec.get(PortsSpecSchema.port_type)
-                if self.outport_connected(oport):
-                    ptype = ([ptype] if not isinstance(ptype,
-                                                       list) else ptype)
-                    key = '{}/{}'.format(self.uid, oport)
-                    # check hdf store for the key
-                    if key not in hf:
-                        raise Exception(
-                            'The task "{}" port "{}" key "{}" not found in'
-                            'the hdf file "{}". Cannot load from cache.'
-                            .format(self.uid, oport, key, filename)
-                        )
-                    if cudf.DataFrame not in ptype:
-                        warnings.warn(
-                            RuntimeWarning,
-                            'Task "{}" port "{}" port type is not set to '
-                            'cudf.DataFrame. Attempting to load port data '
-                            'with cudf.read_hdf.'.format(self.uid, oport))
-                    output_df[oport] = cudf.read_hdf(hf, key)
-        return output_df
-
-    def save_cache(self, output_data: dict):
-        '''Defines the behavior for how to save the output of a node to
-        filesystem cache. Default implementation assumes cudf dataframes.
-
-        :param output_data: The output from :meth:`process`. For saving to hdf
-            requires that the dataframe(s) have `to_hdf` method.
-        '''
-        cache_dir = os.getenv('GREENFLOW_CACHE_DIR', self.cache_dir)
-        os.makedirs(cache_dir, exist_ok=True)
-        filename = cache_dir + '/' + self.uid + '.hdf5'
-        with pd.HDFStore(filename, mode='w') as hf:
-            for oport, odf in output_data.items():
-                # check for to_hdf attribute
-                if not hasattr(odf, 'to_hdf'):
-                    raise Exception(
-                        'Task "{}" port "{}" output object is missing '
-                        '"to_hdf" attribute. Cannot save to cache.'
-                        .format(self.uid, oport))
-
-                dtype = '{}'.format(type(odf)).lower()
-                if 'dataframe' not in dtype:
-                    warnings.warn(
-                        RuntimeWarning,
-                        'Task "{}" port "{}" port type is not a dataframe.'
-                        ' Attempting to save to hdf with "to_hdf" method.'
-                        .format(self.uid, oport))
-                key = '{}/{}'.format(self.uid, oport)
-                odf.to_hdf(hf, key, format='table', data_columns=True)
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/barPlotNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/barPlotNode.py
index 6ff4a062..0399ff2b 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/barPlotNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/barPlotNode.py
@@ -1,17 +1,61 @@
-from greenflow.dataframe_flow import Node
-from bqplot import Axis, LinearScale, DateScale, Figure, OHLC, Bars, Tooltip
-import cupy as cp
-import cudf
+from greenflow.dataframe_flow import Node, PortsSpecSchema
+import mplfinance as mpf
+from ipywidgets import Image
 import dask_cudf
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema, MetaData
-from .._port_type_node import _PortTypesMixin
+import io
+import cudf
+from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
+
+__all__ = ['BarPlotNode']
 
 
-class BarPlotNode(Node):
+class BarPlotNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'stock_in'
         self.OUTPUT_PORT_NAME = 'barplot'
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: ["ipywidgets.Image"]
+            }
+        }
+        cols_required = {"datetime": "datetime64[ns]",
+                         "open": "float64",
+                         "close": "float64",
+                         "high": "float64",
+                         "low": "float64",
+                         "volume": "float64"}
+        retension = {}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: retension
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -33,28 +77,9 @@ def conf_schema(self):
             },
             "required": ["points"],
         }
-        ui = {
-        }
+        ui = {}
         return ConfSchema(json=json, ui=ui)
 
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup_different_output_type(self,
-                                                                 Figure)
-
-    def meta_setup(self):
-        cols_required = {"datetime": "date",
-                         "open": "float64",
-                         "close": "float64",
-                         "high": "float64",
-                         "low": "float64",
-                         "volume": "float64"}
-        required = {
-            self.INPUT_PORT_NAME: cols_required
-        }
-        metadata = MetaData(inports=required,
-                            outports={self.OUTPUT_PORT_NAME: {}})
-        return metadata
-
     def process(self, inputs):
         """
         Takes `datetime`, `open`, `close`, `high`, `volume` columns in the
@@ -71,55 +96,27 @@ def process(self, inputs):
         stock = inputs[self.INPUT_PORT_NAME]
         num_points = self.conf['points']
         stride = max(len(stock) // num_points, 1)
-        label = 'stock'
-        if 'label' in self.conf:
-            label = self.conf['label']
-        sc = LinearScale()
-        sc2 = LinearScale()
-        dt_scale = DateScale()
-        ax_x = Axis(label='Date', scale=dt_scale)
-        ax_y = Axis(label='Price', scale=sc, orientation='vertical',
-                    tick_format='0.0f')
+        buf = io.BytesIO()
         # Construct the marks
-        if (isinstance(stock,
-                       cudf.DataFrame) or isinstance(stock,
-                                                     dask_cudf.DataFrame)):
-            ohlc = OHLC(x=stock['datetime'][::stride].to_array(),
-                        y=cp.asnumpy(stock[['open',
-                                            'high',
-                                            'low',
-                                            'close']].values[::stride, :]),
-                        marker='candle', scales={'x': dt_scale, 'y': sc},
-                        format='ohlc', stroke='blue',
-                        display_legend=True, labels=[label])
-            bar = Bars(x=stock['datetime'][::stride].to_array(),
-                       y=stock['volume'][::stride].to_array(),
-                       scales={'x': dt_scale, 'y': sc2},
-                       padding=0.2)
+        if (isinstance(stock, cudf.DataFrame)
+                or isinstance(stock, dask_cudf.DataFrame)):
+            data_df = stock[[
+                'datetime', 'open', 'high', 'low', 'close', 'volume'
+            ]].iloc[::stride].to_pandas()
         else:
-            ohlc = OHLC(x=stock['datetime'][::stride],
-                        y=stock[['open',
-                                 'high',
-                                 'low', 'close']].values[::stride, :],
-                        marker='candle', scales={'x': dt_scale, 'y': sc},
-                        format='ohlc', stroke='blue',
-                        display_legend=True, labels=[label])
-            bar = Bars(x=stock['datetime'][::stride],
-                       y=stock['volume'][::stride],
-                       scales={'x': dt_scale, 'y': sc2},
-                       padding=0.2)
-        def_tt = Tooltip(fields=['x', 'y'], formats=['%Y-%m-%d', '.2f'])
-        bar.tooltip = def_tt
-        bar.interactions = {
-            'legend_hover': 'highlight_axes',
-            'hover': 'tooltip',
-            'click': 'select',
-         }
-        sc.min = stock['close'].min() - 0.3 * \
-            (stock['close'].max() - stock['close'].min())
-        sc.max = stock['close'].max()
-        sc2.max = stock['volume'].max()*4.0
-        f = Figure(axes=[ax_x, ax_y], marks=[ohlc, bar],
-                   fig_margin={"top": 0, "bottom": 60,
-                               "left": 60, "right": 60})
-        return {self.OUTPUT_PORT_NAME: f}
+            data_df = stock[[
+                'datetime', 'open', 'high', 'low', 'close', 'volume'
+            ]].iloc[::stride]
+        data_df.columns = [
+            'Date', 'Open', 'High', 'Low', 'Close', 'Volume'
+        ]
+        data_df = data_df.set_index('Date')
+        mpf.plot(data_df, type='candle', volume=True, savefig=buf)
+        buf.seek(0)
+        fig = Image(
+            value=buf.read(),
+            format='png',
+            width=600,
+            height=900,
+        )
+        return {self.OUTPUT_PORT_NAME: fig}
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/cumReturnNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/cumReturnNode.py
index c95de500..91bdeeb5 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/cumReturnNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/cumReturnNode.py
@@ -1,30 +1,54 @@
-from greenflow.dataframe_flow import Node
-from bqplot import Axis, LinearScale, DateScale, Figure, Lines, PanZoom
-import dask_cudf
+from greenflow.dataframe_flow import Node, PortsSpecSchema
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+from dask.dataframe import DataFrame as DaskDataFrame
 import cudf
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema, MetaData
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 
-class CumReturnNode(Node, _PortTypesMixin):
+class CumReturnNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'in'
         self.OUTPUT_PORT_NAME = 'cum_return'
-
-    def meta_setup(self):
-        cols_required = {"datetime": "date",
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: ["matplotlib.figure.Figure"]
+            }
+        }
+        cols_required = {"datetime": "datetime64[ns]",
                          "strategy_returns": "float64"}
-        required = {
+        retension = {}
+        meta_inports = {
             self.INPUT_PORT_NAME: cols_required
         }
-        metadata = MetaData(inports=required,
-                            outports={self.OUTPUT_PORT_NAME: {}})
-        return metadata
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup_different_output_type(self,
-                                                                 Figure)
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: retension
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -63,34 +87,30 @@ def process(self, inputs):
 
         """
         input_df = inputs[self.INPUT_PORT_NAME]
-        if isinstance(input_df,  dask_cudf.DataFrame):
+        if isinstance(input_df,  DaskDataFrame):
             input_df = input_df.compute()  # get the computed value
         label = 'stock'
         if 'label' in self.conf:
             label = self.conf['label']
         num_points = self.conf['points']
         stride = max(len(input_df) // num_points, 1)
-        date_co = DateScale()
-        linear_co = LinearScale()
-        yax = Axis(label='Cumulative return', scale=linear_co,
-                   orientation='vertical')
-        xax = Axis(label='Time', scale=date_co, orientation='horizontal')
-        panzoom_main = PanZoom(scales={'x': [date_co]})
+        backend_ = mpl.get_backend()
+        mpl.use("Agg")  # Prevent showing stuff
+
+        f = plt.figure()
         if (isinstance(input_df,
                        cudf.DataFrame) or isinstance(input_df,
-                                                     dask_cudf.DataFrame)):
-            line = Lines(x=input_df['datetime'][::stride].to_array(),
-                         y=(input_df[
+                                                     DaskDataFrame)):
+            plt.plot(input_df['datetime'][::stride].to_array(), (input_df[
                              'strategy_returns'].cumsum())[
-                                 ::stride].to_array(),
-                         scales={'x': date_co, 'y': linear_co},
-                         colors=['blue'], labels=[label], display_legend=True)
+                                 ::stride].to_array(), 'b', label=label)
         else:
-            line = Lines(x=input_df['datetime'][::stride],
-                         y=(input_df[
-                             'strategy_returns'].cumsum())[::stride],
-                         scales={'x': date_co, 'y': linear_co},
-                         colors=['blue'], labels=[label], display_legend=True)
-        new_fig = Figure(marks=[line], axes=[yax, xax], title='P & L',
-                         interaction=panzoom_main)
-        return {self.OUTPUT_PORT_NAME: new_fig}
+            plt.plot(input_df['datetime'][::stride],
+                     (input_df['strategy_returns'].cumsum())[::stride],
+                     'b',
+                     label=label)
+        plt.xlabel("Time")
+        plt.ylabel("Cumulative return")
+        plt.grid(True)
+        mpl.use(backend_)
+        return {self.OUTPUT_PORT_NAME: f}
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/exportXGBoostNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/exportXGBoostNode.py
index 7d608a7a..c2e64fce 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/exportXGBoostNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/exportXGBoostNode.py
@@ -1,42 +1,49 @@
 from greenflow.dataframe_flow import Node
-from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema, MetaData,
-                                                   NodePorts, PortsSpecSchema)
-from xgboost import Booster
+from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema,
+                                                      PortsSpecSchema)
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
 from greenflow.dataframe_flow.util import get_file_path
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 
-class XGBoostExportNode(Node):
+class XGBoostExportNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'model_in'
         self.OUTPUT_PORT_NAME = 'filename'
-
-    def meta_setup(self):
-        required = {self.INPUT_PORT_NAME: {}}
-        input_meta = self.get_input_meta()
-        output_cols = {
-            self.OUTPUT_PORT_NAME: {}
-        }
-        if (self.INPUT_PORT_NAME in input_meta):
-            output_cols = {
-                self.OUTPUT_PORT_NAME: input_meta[self.INPUT_PORT_NAME]
-            }
-        metadata = MetaData(inports=required, outports=output_cols)
-        return metadata
-
-    def ports_setup(self):
         port_type = PortsSpecSchema.port_type
-        input_ports = {
+        port_inports = {
             self.INPUT_PORT_NAME: {
-                port_type: [Booster, dict]
+                port_type: ["xgboost.Booster", "builtins.dict"]
             }
         }
-        output_ports = {
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: ["builtins.str"]
+            }
+        }
+        cols_required = {}
+        addition = {}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
             self.OUTPUT_PORT_NAME: {
-                port_type: str
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: addition
             }
         }
-        return NodePorts(inports=input_ports, outports=output_ports)
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -45,9 +52,11 @@ def conf_schema(self):
             "description": """Export the xgboost model to a file
             """,
             "properties": {
-                "path":  {
+                "path": {
                     "type": "string",
-                    "description": """The output filepath for the xgboost model"""
+                    "description":
+                    """The output filepath for the xgboost
+                     model"""
                 }
             },
             "required": ["path"],
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/importanceCurve.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/importanceCurve.py
index b14f32b8..16065d91 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/importanceCurve.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/importanceCurve.py
@@ -1,39 +1,49 @@
-from greenflow.dataframe_flow import Node
-from bqplot import Axis, LinearScale,  Figure, OrdinalScale, Bars
-from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema, NodePorts,
-                                                   MetaData,
-                                                   PortsSpecSchema)
-from xgboost import Booster
+from greenflow.dataframe_flow import Node, PortsSpecSchema
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+import numpy as np
+from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 
-class ImportanceCurveNode(Node):
+class ImportanceCurveNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'in'
         self.OUTPUT_PORT_NAME = 'importance_curve'
-
-    def meta_setup(self):
-        cols_required = {}
-        required = {
-            self.INPUT_PORT_NAME: cols_required
-        }
-        metadata = MetaData(inports=required,
-                            outports={self.OUTPUT_PORT_NAME: {}})
-        return metadata
-
-    def ports_setup(self):
         port_type = PortsSpecSchema.port_type
-        output_ports = {
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: ["xgboost.Booster", "builtins.dict"]
+            }
+        }
+        port_outports = {
             self.OUTPUT_PORT_NAME: {
-                port_type: Figure
+                port_type: ["matplotlib.figure.Figure"]
             }
         }
-        input_ports = {
-            self.INPUT_PORT_NAME: {
-                port_type: [Booster, dict]
+        cols_required = {}
+        retension = {}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: retension
             }
         }
-        return NodePorts(inports=input_ports, outports=output_ports)
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -83,8 +93,12 @@ def process(self, inputs):
         model = inputs[self.INPUT_PORT_NAME]
         if isinstance(model, dict):
             model = model['booster']
-        x_ord = OrdinalScale()
-        y_sc = LinearScale()
+        # x_ord = OrdinalScale()
+        # y_sc = LinearScale()
+        backend_ = mpl.get_backend()
+        mpl.use("Agg")  # Prevent showing stuff
+        f = plt.figure()
+
         data = model.get_score(importance_type=self.conf.get('type', 'gain'))
         x_values = []
         y_values = []
@@ -92,9 +106,17 @@ def process(self, inputs):
             x_values.append(key)
             y_values.append(data[key])
 
-        bar = Bars(x=x_values, y=y_values, scales={'x': x_ord, 'y': y_sc})
-        ax_x = Axis(scale=x_ord)
-        ax_y = Axis(scale=y_sc, tick_format='0.2f', orientation='vertical')
-        newFig = Figure(marks=[bar], axes=[ax_x, ax_y], padding_x=0.025,
-                        padding_y=0.025)
-        return {self.OUTPUT_PORT_NAME: newFig}
+        width = 0.35  # the width of the bars
+        x = np.arange(len(x_values))
+        plt.bar(x - width/2, y_values, width, label='Feature Importance')
+        plt.xticks(x, x_values, rotation='vertical')
+        # ax = f.get_axes()[0]
+        # ax.set_xticks(x)
+        # ax.set_xticklabels(x_values)
+        plt.xlabel('Features')
+        plt.ylabel('Importance')
+        plt.grid(True)
+        f.set_figwidth(15)
+        f.set_figheight(8)
+        mpl.use(backend_)
+        return {self.OUTPUT_PORT_NAME: f}
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/linePlotNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/linePlotNode.py
index bfd092c0..dcdeb2bb 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/linePlotNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/linePlotNode.py
@@ -1,16 +1,53 @@
-from greenflow.dataframe_flow import Node
-from bqplot import Axis, LinearScale, DateScale, Figure, Lines, PanZoom
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema, MetaData
+from greenflow.dataframe_flow import Node, PortsSpecSchema
+import matplotlib as mpl
+import matplotlib.pyplot as plt
+from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
 import cudf
 import dask_cudf
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 
-class LinePlotNode(Node, _PortTypesMixin):
+class LinePlotNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'in'
         self.OUTPUT_PORT_NAME = 'lineplot'
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: ["matplotlib.figure.Figure"]
+            }
+        }
+        cols_required = {"datetime": "datetime64[ns]"}
+        retension = {}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: retension
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         color_strings = ['black', 'yellow', 'blue',
@@ -63,19 +100,6 @@ def conf_schema(self):
         else:
             return ConfSchema(json=json, ui=ui)
 
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup_different_output_type(self,
-                                                                 Figure)
-
-    def meta_setup(self):
-        cols_required = {"datetime": "date"}
-        required = {
-            self.INPUT_PORT_NAME: cols_required
-        }
-        metadata = MetaData(inports=required,
-                            outports={self.OUTPUT_PORT_NAME: {}})
-        return metadata
-
     def process(self, inputs):
         """
         Plot the lines from the input dataframe. The plotted lines are the
@@ -96,12 +120,10 @@ def process(self, inputs):
 
         num_points = self.conf['points']
         stride = max(len(input_df) // num_points, 1)
-        date_co = DateScale()
-        linear_co = LinearScale()
-        yax = Axis(label='', scale=linear_co, orientation='vertical')
-        xax = Axis(label='Time', scale=date_co, orientation='horizontal')
-        panzoom_main = PanZoom(scales={'x': [date_co]})
-        lines = []
+        backend_ = mpl.get_backend()
+        mpl.use("Agg")  # Prevent showing stuff
+        f = plt.figure()
+
         for line in self.conf['lines']:
             col_name = line['column']
             label_name = line['label']
@@ -109,19 +131,14 @@ def process(self, inputs):
             if (isinstance(input_df,
                            cudf.DataFrame) or isinstance(input_df,
                                                          dask_cudf.DataFrame)):
-                line = Lines(x=input_df['datetime'][::stride].to_array(),
-                             y=input_df[col_name][::stride].to_array(),
-                             scales={'x': date_co, 'y': linear_co},
-                             colors=[color],
-                             labels=[label_name], display_legend=True)
+                line = plt.plot(input_df['datetime'][::stride].to_array(),
+                                input_df[col_name][::stride].to_array(),
+                                color=color, label=label_name)
             else:
-                line = Lines(x=input_df['datetime'][::stride],
-                             y=input_df[col_name][::stride],
-                             scales={'x': date_co, 'y': linear_co},
-                             colors=[color],
-                             labels=[label_name], display_legend=True)
-
-            lines.append(line)
-        new_fig = Figure(marks=lines, axes=[yax, xax],
-                         title=self.conf['title'], interaction=panzoom_main)
-        return {self.OUTPUT_PORT_NAME: new_fig}
+                line = plt.plot(input_df['datetime'][::stride],
+                                input_df[col_name][::stride],
+                                color=color, label=label_name)
+        plt.grid(True)
+        plt.legend()
+        mpl.use(backend_)
+        return {self.OUTPUT_PORT_NAME: f}
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/outCsvNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/outCsvNode.py
index 758ea2d8..4c03cf36 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/outCsvNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/outCsvNode.py
@@ -1,23 +1,52 @@
-from greenflow.dataframe_flow import Node
+from greenflow.dataframe_flow import Node, PortsSpecSchema
 import dask_cudf
 from greenflow.dataframe_flow.util import get_file_path
 from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 
-class OutCsvNode(Node, _PortTypesMixin):
+class OutCsvNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'df_in'
         self.OUTPUT_PORT_NAME = 'df_out'
-
-    def meta_setup(self):
-        required = {}
-        return _PortTypesMixin.meta_setup(self, required=required)
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:df_in}"
+            }
+        }
+        cols_required = {}
+        addition = {}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: addition
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         input_meta = self.get_input_meta()
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/rocCurveNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/rocCurveNode.py
index f65f0713..14f78c66 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/rocCurveNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/rocCurveNode.py
@@ -1,21 +1,41 @@
 from greenflow.dataframe_flow import Node
-from bqplot import Axis, LinearScale,  Figure, Lines, PanZoom
+# from bqplot import Axis, LinearScale,  Figure, Lines, PanZoom
 import dask_cudf
-import cudf
-from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema, PortsSpecSchema,
-                                                   MetaData)
-from .._port_type_node import _PortTypesMixin
+import matplotlib as mpl
+import matplotlib.pyplot as plt
 from sklearn import metrics
+import cudf
+from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema,
+                                                      PortsSpecSchema)
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 
-class RocCurveNode(Node, _PortTypesMixin):
+class RocCurveNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'in'
         self.OUTPUT_PORT_NAME = 'roc_curve'
         self.OUTPUT_VALUE_NAME = 'value'
-
-    def meta_setup(self):
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: ["matplotlib.figure.Figure"]
+            },
+            self.OUTPUT_VALUE_NAME: {
+                port_type: ["builtins.float"]
+            }
+        }
         cols_required = {}
         icols = self.get_input_meta()
         if 'label' in self.conf:
@@ -24,19 +44,28 @@ def meta_setup(self):
             cols_required[label] = labeltype
         if 'prediction' in self.conf:
             cols_required[self.conf['prediction']] = None
-        required = {
+        retension = {}
+        meta_inports = {
             self.INPUT_PORT_NAME: cols_required
         }
-        metadata = MetaData(inports=required,
-                            outports={self.OUTPUT_PORT_NAME: {},
-                                      self.OUTPUT_VALUE_NAME: {}})
-        return metadata
-
-    def ports_setup(self):
-        ports = _PortTypesMixin.ports_setup_different_output_type(self, Figure)
-        ports.outports[self.OUTPUT_VALUE_NAME] = {PortsSpecSchema.port_type:
-                                                  float}
-        return ports
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: retension
+            },
+            self.OUTPUT_VALUE_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: retension
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -95,23 +124,35 @@ def process(self, inputs):
                                             pred_col)
         auc_value = metrics.auc(fpr, tpr)
         out = {}
+        backend_ = mpl.get_backend()
+        mpl.use("Agg")  # Prevent showing stuff
+
+        f = plt.figure()
 
         if self.outport_connected(self.OUTPUT_PORT_NAME):
-            linear_x = LinearScale()
-            linear_y = LinearScale()
-            yax = Axis(label='True Positive Rate', scale=linear_x,
-                       orientation='vertical')
-            xax = Axis(label='False Positive Rate', scale=linear_y,
-                       orientation='horizontal')
-            panzoom_main = PanZoom(scales={'x': [linear_x]})
+            # linear_x = LinearScale()
+            # linear_y = LinearScale()
+            # yax = Axis(label='True Positive Rate', scale=linear_x,
+            #            orientation='vertical')
+            # xax = Axis(label='False Positive Rate', scale=linear_y,
+            #            orientation='horizontal')
+            # panzoom_main = PanZoom(scales={'x': [linear_x]})
             curve_label = 'ROC (area = {:.2f})'.format(auc_value)
-            line = Lines(x=fpr, y=tpr,
-                         scales={'x': linear_x, 'y': linear_y},
-                         colors=['blue'], labels=[curve_label],
-                         display_legend=True)
-            new_fig = Figure(marks=[line], axes=[yax, xax], title='ROC Curve',
-                             interaction=panzoom_main)
-            out.update({self.OUTPUT_PORT_NAME: new_fig})
+            plt.plot(fpr, tpr, color='blue', label=curve_label)
+            # line = Lines(x=fpr, y=tpr,
+            #              scales={'x': linear_x, 'y': linear_y},
+            #              colors=['blue'], labels=[curve_label],
+            #              display_legend=True)
+            # new_fig = Figure(marks=[line], axes=[yax, xax],
+            #                  title='ROC Curve',
+            #                  interaction=panzoom_main)
+            plt.xlabel('False Positive Rate')
+            plt.ylabel('True Positive Rate')
+            plt.grid(True)
+            plt.title('ROC Curve')
+            plt.legend()
+            mpl.use(backend_)
+            out.update({self.OUTPUT_PORT_NAME: f})
         if self.outport_connected(self.OUTPUT_VALUE_NAME):
             out.update({self.OUTPUT_VALUE_NAME: float(auc_value)})
         return out
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/scatterPlotNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/scatterPlotNode.py
index f94a2a54..e4eadca7 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/scatterPlotNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/scatterPlotNode.py
@@ -1,10 +1,12 @@
-from greenflow.dataframe_flow import Node
+from greenflow.dataframe_flow import Node, PortsSpecSchema
 from bqplot import (Axis, LinearScale,  Figure,
                     DateScale, ColorScale, ColorAxis, Scatter)
 import dask_cudf
 import cudf
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema, MetaData
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 __all__ = ["ScatterPlotNode"]
 
@@ -15,13 +17,26 @@
 }
 
 
-class ScatterPlotNode(Node, _PortTypesMixin):
+class ScatterPlotNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'in'
         self.OUTPUT_PORT_NAME = 'scatter_plot'
-
-    def meta_setup(self):
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: ["bqplot.Figure"]
+            }
+        }
         cols_required = {}
         if 'col_x' in self.conf:
             cols_required[self.conf['col_x']] = None
@@ -29,16 +44,24 @@ def meta_setup(self):
             cols_required[self.conf['col_y']] = None
         if 'col_color' in self.conf:
             cols_required[self.conf['col_color']] = None
-        required = {
+        retension = {}
+        meta_inports = {
             self.INPUT_PORT_NAME: cols_required
         }
-        metadata = MetaData(inports=required,
-                            outports={self.OUTPUT_PORT_NAME: {}})
-        return metadata
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup_different_output_type(self,
-                                                                 Figure)
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: retension
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -82,8 +105,7 @@ def conf_schema(self):
             },
             "required": ["points", "title", "col_x", "col_y"],
         }
-        ui = {
-        }
+        ui = {}
         input_meta = self.get_input_meta()
         if self.INPUT_PORT_NAME in input_meta:
             col_from_inport = input_meta[self.INPUT_PORT_NAME]
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/sharpeRatioNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/sharpeRatioNode.py
index 83d40957..489cd8fb 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/sharpeRatioNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/analysis/sharpeRatioNode.py
@@ -1,28 +1,51 @@
-from greenflow.dataframe_flow import Node
+from greenflow.dataframe_flow import Node, PortsSpecSchema
 import math
 import dask_cudf
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema, MetaData
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 
-class SharpeRatioNode(Node, _PortTypesMixin):
+class SharpeRatioNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'stock_in'
         self.OUTPUT_PORT_NAME = 'sharpe_out'
-
-    def meta_setup(self):
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: ["builtins.float"]
+            }
+        }
         cols_required = {"strategy_returns": "float64"}
-        required = {
+        retension = {}
+        meta_inports = {
             self.INPUT_PORT_NAME: cols_required
         }
-        metadata = MetaData(inports=required,
-                            outports={self.OUTPUT_PORT_NAME: {}})
-        return metadata
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup_different_output_type(self,
-                                                                 float)
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: retension
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/dataloader/classificationGenerator.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/dataloader/classificationGenerator.py
index cbe26b96..f9ddfa50 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/dataloader/classificationGenerator.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/dataloader/classificationGenerator.py
@@ -1,20 +1,19 @@
 from greenflow.dataframe_flow import Node
-from greenflow.dataframe_flow.portsSpecSchema import (PortsSpecSchema,
-                                                   NodePorts,
-                                                   MetaData,
-                                                   ConfSchema)
+from greenflow.dataframe_flow.portsSpecSchema import (
+    PortsSpecSchema, NodePorts, ConfSchema)
+from greenflow.dataframe_flow.metaSpec import MetaData
 import cudf
 import dask_cudf
 import cuml
 import copy
 import cuml.dask.datasets.classification
-from .._port_type_node import _PortTypesMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 CUDF_PORT_NAME = 'cudf_out'
 DASK_CUDF_PORT_NAME = 'dask_cudf_out'
 
 
-class ClassificationData(_PortTypesMixin, Node):
+class ClassificationData(NodeHDFCacheMixin, Node):
 
     def ports_setup(self):
         input_ports = {}
@@ -28,9 +27,6 @@ def ports_setup(self):
         }
         return NodePorts(inports=input_ports, outports=output_ports)
 
-    def init(self):
-        pass
-
     def meta_setup(self):
         column_types = {}
         total_features = self.conf.get("n_features", 20)
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/dataloader/csvStockLoader.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/dataloader/csvStockLoader.py
index 27b37c31..f1d6fe1a 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/dataloader/csvStockLoader.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/dataloader/csvStockLoader.py
@@ -1,20 +1,19 @@
 from greenflow.dataframe_flow import Node
-from greenflow.dataframe_flow.portsSpecSchema import (PortsSpecSchema,
-                                                   NodePorts,
-                                                   MetaData,
-                                                   ConfSchema)
+from greenflow.dataframe_flow.portsSpecSchema import (
+    PortsSpecSchema, NodePorts, ConfSchema)
+from greenflow.dataframe_flow.metaSpec import MetaData
 import cudf
 import dask_cudf
 import pandas as pd
 from greenflow.dataframe_flow.util import get_file_path
-from .._port_type_node import _PortTypesMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 CUDF_PORT_NAME = 'cudf_out'
 DASK_CUDF_PORT_NAME = 'dask_cudf_out'
 PANDAS_PORT_NAME = 'pandas_out'
 
 
-class CsvStockLoader(_PortTypesMixin, Node):
+class CsvStockLoader(NodeHDFCacheMixin, Node):
 
     def ports_setup(self):
         input_ports = {}
@@ -31,17 +30,16 @@ def ports_setup(self):
         }
         return NodePorts(inports=input_ports, outports=output_ports)
 
-    def init(self):
-        pass
-
     def meta_setup(self):
-        column_types = {"datetime": "date",
-                        "asset": "int64",
-                        "volume": "float64",
-                        "close": "float64",
-                        "open": "float64",
-                        "high": "float64",
-                        "low": "float64"}
+        column_types = {
+            "datetime": "datetime64[ns]",
+            "open": "float64",
+            "close": "float64",
+            "high": "float64",
+            "low": "float64",
+            "asset": "int64",
+            "volume": "float64"
+        }
         out_cols = {
             CUDF_PORT_NAME: column_types,
             DASK_CUDF_PORT_NAME: column_types,
@@ -96,7 +94,7 @@ def process(self, inputs):
             # construct the standard datetime str
             df['DTE'] = ymd[0].str.cat(
                 ymd[1],
-                '-').str.cat(ymd[2], '-').astype('datetime64[ms]')
+                '-').str.cat(ymd[2], '-').astype('datetime64[ns]')
             df = df[['DTE', 'OPEN', 'CLOSE', 'HIGH', 'LOW', 'SM_ID', 'VOLUME']]
             df['VOLUME'] /= 1000
             # change the names
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/dataloader/stockNameLoader.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/dataloader/stockNameLoader.py
index 3def2d73..7b0a58c8 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/dataloader/stockNameLoader.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/dataloader/stockNameLoader.py
@@ -1,19 +1,19 @@
 from greenflow.dataframe_flow import Node
 import cudf
-from greenflow.dataframe_flow.portsSpecSchema import (PortsSpecSchema,
-                                                   NodePorts,
-                                                   MetaData,
-                                                   ConfSchema)
+from greenflow.dataframe_flow.portsSpecSchema import (
+    PortsSpecSchema, NodePorts, ConfSchema)
+from greenflow.dataframe_flow.metaSpec import MetaData
 from ..cache import CACHE_NAME
 from greenflow.dataframe_flow.util import get_file_path
 
 from .stockMap import StockMap
-from .._port_type_node import _PortTypesMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
+
 STOCK_NAME_PORT_NAME = 'stock_name'
 STOCK_MAP_PORT_NAME = 'map_data'
 
 
-class StockNameLoader(_PortTypesMixin, Node):
+class StockNameLoader(NodeHDFCacheMixin, Node):
 
     def _compute_hash_key(self):
         return hash((self.uid, self.conf['file']))
@@ -30,9 +30,6 @@ def ports_setup(self):
         }
         return NodePorts(inports=input_ports, outports=output_ports)
 
-    def init(self):
-        pass
-
     def meta_setup(self):
         required = {}
         column_types = {"asset": "int64",
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/forestInference.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/forestInference.py
index 30d6356d..928d643e 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/forestInference.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/forestInference.py
@@ -1,99 +1,77 @@
+from cuml import ForestInference
 from greenflow.dataframe_flow import Node
-import cudf
-import dask_cudf
-from .._port_type_node import _PortTypesMixin
 from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema,
-                                                   MetaData,
-                                                   PortsSpecSchema, NodePorts)
-import copy
-from cuml import ForestInference
+                                                      PortsSpecSchema)
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
 from greenflow.dataframe_flow.util import get_file_path
-
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 __all__ = ['ForestInferenceNode']
 
 
-class ForestInferenceNode(_PortTypesMixin, Node):
+class ForestInferenceNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.delayed_process = True
         self.INPUT_PORT_NAME = 'data_in'
         self.INPUT_PORT_MODEL_NAME = 'model_file'
         self.OUTPUT_PORT_NAME = 'out'
-
-    def ports_setup_from_types(self, types):
         port_type = PortsSpecSchema.port_type
-        input_ports = {
+        port_inports = {
             self.INPUT_PORT_NAME: {
-                port_type: types
+                port_type: [
+                    "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
             },
             self.INPUT_PORT_MODEL_NAME: {
-                port_type: str
+                port_type: ['builtins.str']
             }
+
         }
-        output_ports = {
+        port_outports = {
             self.OUTPUT_PORT_NAME: {
-                port_type: types
+                port_type: "${port:data_in}"
             }
         }
-        input_connections = self.get_connected_inports()
-        if self.INPUT_PORT_NAME in input_connections:
-            determined_type = input_connections[self.INPUT_PORT_NAME]
-            input_ports.update({self.INPUT_PORT_NAME:
-                                {port_type: determined_type}})
-            output_ports.update({self.OUTPUT_PORT_NAME:
-                                {port_type: determined_type}})
-            return NodePorts(inports=input_ports,
-                             outports=output_ports)
-        else:
-            return NodePorts(inports=input_ports, outports=output_ports)
-
-    def meta_setup(self):
-        input_meta = self.get_input_meta()
-        required = {self.INPUT_PORT_NAME: {},
-                    self.INPUT_PORT_MODEL_NAME: {}}
+        meta_inports = {
+            self.INPUT_PORT_NAME: {},
+            self.INPUT_PORT_MODEL_NAME: {}
+        }
         predict = self.conf.get('prediction', 'predict')
-        output_cols = {
-            self.OUTPUT_PORT_NAME: {predict: None}
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: {predict: None}
+            }
         }
-        if (self.INPUT_PORT_NAME in input_meta
-                and self.INPUT_PORT_MODEL_NAME in input_meta):
-            col_from_inport = input_meta[self.INPUT_PORT_NAME]
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
+
+    def update(self):
+        TemplateNodeMixin.update(self)
+        input_meta = self.get_input_meta()
+        meta_inports = self.template_meta_setup().inports
+        if self.INPUT_PORT_MODEL_NAME in input_meta:
             if 'train' in input_meta[self.INPUT_PORT_MODEL_NAME]:
                 required_cols = input_meta[self.INPUT_PORT_MODEL_NAME]['train']
             else:
                 required_cols = {}
-            predict = self.conf.get('prediction', 'predict')
-            col_from_inport[predict] = None  # the type is not determined
-            required = {self.INPUT_PORT_NAME: required_cols,
-                        self.INPUT_PORT_MODEL_NAME: {}}
-            output_cols = {
-                self.OUTPUT_PORT_NAME: col_from_inport,
-            }
-            metadata = MetaData(inports=required, outports=output_cols)
-            return metadata
-        elif (self.INPUT_PORT_NAME not in input_meta and
-              self.INPUT_PORT_MODEL_NAME in input_meta):
-            if 'train' in input_meta[self.INPUT_PORT_MODEL_NAME]:
-                required_cols = input_meta[self.INPUT_PORT_MODEL_NAME]['train']
+            meta_inports[self.INPUT_PORT_NAME] = required_cols
+        else:
+            if self.INPUT_PORT_NAME in input_meta:
+                col_from_inport = input_meta[self.INPUT_PORT_NAME]
             else:
-                required_cols = {}
-            predict = self.conf.get('prediction', 'predict')
-            col_from_inport = copy.copy(required_cols)
-            col_from_inport[predict] = None
-            # the type is not determined
-            required = {self.INPUT_PORT_NAME: required_cols,
-                        self.INPUT_PORT_MODEL_NAME: {}}
-            output_cols = {
-                self.OUTPUT_PORT_NAME: col_from_inport
-            }
-            metadata = MetaData(inports=required, outports=output_cols)
-            return metadata
-        elif (self.INPUT_PORT_NAME in input_meta and
-              self.INPUT_PORT_MODEL_NAME not in input_meta):
-            cols_required = {}
-            col_from_inport = input_meta[self.INPUT_PORT_NAME]
+                col_from_inport = {}
             enums = [col for col in col_from_inport.keys()]
             if 'columns' in self.conf:
                 if self.conf.get('include', True):
@@ -103,37 +81,14 @@ def meta_setup(self):
                                        if col not in self.conf['columns']]
                 for col in included_colums:
                     if col in col_from_inport:
-                        cols_required[col] = col_from_inport[col]
+                        meta_inports[
+                            self.INPUT_PORT_NAME][col] = col_from_inport[col]
                     else:
-                        cols_required[col] = None
-            predict = self.conf.get('prediction', 'predict')
-            col_from_inport[predict] = None  # the type is not determined
-            required = {
-                 self.INPUT_PORT_NAME: cols_required,
-            }
-            output_cols = {
-                self.OUTPUT_PORT_NAME: col_from_inport
-            }
-            metadata = MetaData(inports=required, outports=output_cols)
-            return metadata
-        elif (self.INPUT_PORT_NAME not in input_meta and
-              self.INPUT_PORT_MODEL_NAME not in input_meta):
-            if 'columns' in self.conf:
-                if self.conf.get('include', True):
-                    included_colums = self.conf['columns']
-                    cols_required = {}
-                    for col in included_colums:
-                        cols_required[col] = None
-                    required = {
-                        self.INPUT_PORT_NAME: cols_required,
-                    }
-        metadata = MetaData(inports=required, outports=output_cols)
-        return metadata
-
-    def ports_setup(self):
-        types = [cudf.DataFrame,
-                 dask_cudf.DataFrame]
-        return self.ports_setup_from_types(types)
+                        meta_inports[self.INPUT_PORT_NAME][col] = None
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=None
+        )
 
     def conf_schema(self):
         json = {
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/gridRandomSearchNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/gridRandomSearchNode.py
index be98504c..285964c7 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/gridRandomSearchNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/gridRandomSearchNode.py
@@ -1,16 +1,23 @@
-from greenflow.plugin_nodes.util.contextCompositeNode import ContextCompositeNode
-from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema, MetaData,
-                                                   PortsSpecSchema, NodePorts)
-from greenflow.dataframe_flow.cache import CACHE_SCHEMA
-from greenflow.dataframe_flow import TaskGraph
-from greenflow.dataframe_flow import Node
-from greenflow.dataframe_flow.util import get_file_path
-from greenflow.dataframe_flow.taskSpecSchema import TaskSpecSchema
 from jsonpath_ng import parse
 import uuid
 import cudf
 import pandas
 from copy import deepcopy
+import ray
+from ray import tune
+
+from greenflow.plugin_nodes.util.contextCompositeNode import \
+    ContextCompositeNode
+from greenflow.plugin_nodes.util.compositeNode import (group_ports, _get_node,
+                                                       _get_port)
+from greenflow.dataframe_flow.portsSpecSchema import (
+    ConfSchema, PortsSpecSchema, NodePorts)
+from greenflow.dataframe_flow.metaSpec import MetaData
+from greenflow.dataframe_flow import TaskGraph
+from greenflow.dataframe_flow import Node
+from greenflow.dataframe_flow.util import get_file_path
+from greenflow.dataframe_flow.taskSpecSchema import TaskSpecSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
 
 __all__ = ["GridRandomSearchNode"]
 
@@ -303,6 +310,254 @@
 }
 
 
+class SeriliazableNode(object):
+
+    def __init__(self, uid, meta_data, ports_data):
+        self.meta_data = meta_data
+        self.ports_data = ports_data
+        self.uid = uid
+
+    def ports_setup(self):
+        return self.ports_data
+
+    def meta_setup(self):
+        return self.meta_data
+
+
+def get_clean_inputs(conf, task_graph, inputs):
+    update_inputs = []
+    if 'input' in conf:
+        # group input ports by node id
+        inp_groups = group_ports(conf['input'])
+        for inp in inp_groups.keys():
+            if inp in task_graph:
+                inputNode = task_graph[inp]
+                replaced_ports = set(inp_groups[inp])
+                for oldInput in inputNode.inputs:
+                    if oldInput['to_port'] in replaced_ports:
+                        # we want to disconnect this old one and
+                        # connect to external node
+                        if True:
+                            for externalInput in inputs:
+                                if (_get_node(
+                                    externalInput[
+                                        'to_port']) == inputNode.uid
+                                        and _get_port(
+                                            externalInput[
+                                                'to_port']) == oldInput[
+                                                    'to_port']):
+                                    newInput = {}
+                                    newInput['to_port'] = externalInput[
+                                        'to_port']
+                                    newInput['from_port'] = externalInput[
+                                        'from_port']
+                                    e_node = externalInput['from_node']
+                                    newInput['from_node'] = SeriliazableNode(
+                                        e_node.uid,
+                                        e_node.meta_setup(),
+                                        e_node.ports_setup())
+                                    update_inputs.append(newInput)
+    return update_inputs
+
+
+def update_conf_for_search(conf, replaceObj, task_graph, config):
+    # find the other replacment conf
+    if task_graph:
+        for task in task_graph:
+            key = task.get('id')
+            newid = key
+            conf_obj = task.get('conf')
+            if newid in replaceObj:
+                replaceObj[newid].update({'conf': conf_obj})
+            else:
+                replaceObj[newid] = {}
+                replaceObj[newid].update({'conf': conf_obj})
+    # replace the numbers from the context
+    if 'context' in conf:
+        for key in conf['context'].keys():
+            val = conf['context'][key]['value']
+            if key in config:
+                val = config[key]
+            for map_obj in conf['context'][key]['map']:
+                xpath = map_obj['xpath']
+                expr = parse(xpath)
+                expr.update(replaceObj, val)
+
+
+def get_search_fun(data_store, conf, inputs):
+
+    def search_fun(config, checkpoint_dir=None):
+        myinputs = {}
+        for key in data_store.keys():
+            v = ray.get(data_store[key])
+            if isinstance(v, pandas.DataFrame):
+                myinputs[key] = cudf.from_pandas(v)
+            else:
+                myinputs[key] = v
+        task_graph = TaskGraph.load_taskgraph(
+            get_file_path(conf['taskgraph']))
+        task_graph._build()
+
+        outputLists = []
+        replaceObj = {}
+        input_feeders = []
+
+        def inputNode_fun(inputNode, in_ports):
+            inports = inputNode.ports_setup().inports
+
+            class InputFeed(TemplateNodeMixin, Node):
+
+                def meta_setup(self):
+                    output = {}
+                    for inp in inputNode.inputs:
+                        # output[inp['to_port']] = inp[
+                        #     'from_node'].meta_setup().outports[
+                        #         inp['from_port']]
+                        output[inp['to_port']] = inp[
+                            'from_node'].meta_setup().outports[
+                                inp['from_port']]
+                    # it will be something like { input_port: columns }
+                    return MetaData(inports={}, outports=output)
+
+                def ports_setup(self):
+                    # it will be something like { input_port: types }
+                    return NodePorts(inports={}, outports=inports)
+
+                def conf_schema(self):
+                    return ConfSchema()
+
+                def update(self):
+                    TemplateNodeMixin.update(self)
+
+                def process(self, empty):
+                    output = {}
+                    for key in inports.keys():
+                        if (inputNode.uid+'@'+key
+                                in myinputs):
+                            output[key] = myinputs[
+                                inputNode.uid+'@'+key]
+                    return output
+
+            uni_id = str(uuid.uuid1())
+            obj = {
+                TaskSpecSchema.task_id: uni_id,
+                TaskSpecSchema.conf: {},
+                TaskSpecSchema.node_type: InputFeed,
+                TaskSpecSchema.inputs: []
+            }
+            input_feeders.append(obj)
+            newInputs = {}
+            for key in inports.keys():
+                if inputNode.uid+'@'+key in myinputs:
+                    newInputs[key] = uni_id+'.'+key
+            for inp in inputNode.inputs:
+                if inp['to_port'] not in in_ports:
+                    # need to keep the old connections
+                    newInputs[inp['to_port']] = (
+                        inp['from_node'].uid + '.' + inp['from_port'])
+            replaceObj.update({inputNode.uid: {
+                TaskSpecSchema.inputs: newInputs}
+            })
+
+        def outNode_fun(outNode, out_ports):
+            pass
+        outputLists = conf['metrics']
+
+        make_sub_graph_connection(conf, inputs, task_graph, inputNode_fun,
+                                  outNode_fun)
+
+        task_graph.extend(input_feeders)
+        update_conf_for_search(conf, replaceObj, task_graph, config)
+        result = task_graph.run(outputLists, replace=replaceObj)
+        metric_report = {item: result[item] for item in outputLists}
+        tune.report(**metric_report)
+    return search_fun
+
+
+def make_sub_graph_connection(conf, inputs, task_graph, inputNode_fun,
+                              outNode_fun):
+    """
+    connects the current composite node's inputs and outputs to
+    the subgraph-task_graph's inputs and outputs.
+    inputNode_fun has subgraph inputNode and all the input ports
+    as argument, it processes the inputNode logics
+    outputNode_fun has subgraph outputNode and all the outpout ports
+    as argument, it processes the outNode logics
+    """
+    all_inputs = []
+    all_outputs = []
+    extra_updated = set()
+    extra_roots = []
+    if 'input' in conf:
+        # group input ports by node id
+        inp_groups = group_ports(conf['input'])
+        for inp in inp_groups.keys():
+            if inp in task_graph:
+                inputNode = task_graph[inp]
+                update_inputs = []
+                replaced_ports = set(inp_groups[inp])
+                for oldInput in inputNode.inputs:
+                    if oldInput['to_port'] in replaced_ports:
+                        # we want to disconnect this old one and
+                        # connect to external node
+                        if True:
+                            for externalInput in inputs:
+                                if (_get_node(
+                                    externalInput[
+                                        'to_port']) == inputNode.uid
+                                        and _get_port(
+                                            externalInput[
+                                                'to_port']) == oldInput[
+                                                    'to_port']):
+                                    newInput = {}
+                                    newInput['to_port'] = _get_port(
+                                        externalInput['to_port'])
+                                    newInput['from_port'] = externalInput[
+                                        'from_port']
+                                    newInput['from_node'] = externalInput[
+                                        'from_node']
+                                    update_inputs.append(newInput)
+                    else:
+                        update_inputs.append(oldInput)
+                inputNode.inputs = update_inputs
+
+                # add all the `updated` parents to the set
+                for i in inputNode.inputs:
+                    if isinstance(i['from_node'], SeriliazableNode):
+                        extra_updated.add(i['from_node'])
+                # if all the parents are updated, this is
+                # a new root node
+                if all([
+                        isinstance(i['from_node'], SeriliazableNode)
+                        for i in inputNode.inputs
+                ]):
+                    extra_roots.append(inputNode)
+
+                all_inputs.append((inputNode, inp))
+
+    if 'output' in conf:
+        oup_groups = group_ports(conf['output'])
+        for oup in oup_groups.keys():
+            if oup in task_graph:
+                outNode = task_graph[oup]
+                # we do not disconnect anything here, as we take extra
+                # outputs for composite node.
+                # Node, we rely on the fact that taskgraph.run method
+                # will remove the output collector from taskgraph if
+                # the outputlist is set
+                all_outputs.append((outNode, oup))
+                # outNode_fun(outNode, oup_groups[oup])
+
+    # update all the nodes and cache it
+    task_graph.breadth_first_update(extra_roots=extra_roots,
+                                    extra_updated=extra_updated)
+    for innode in all_inputs:
+        inputNode_fun(innode[0], inp_groups[innode[1]])
+    for outnode in all_outputs:
+        # inputNode_fun(innode[0], inp_groups[innode[1]])
+        outNode_fun(outnode[0], oup_groups[outnode[1]])
+
+
 class GridRandomSearchNode(ContextCompositeNode):
 
     def init(self):
@@ -312,9 +567,12 @@ def ports_setup(self):
         return ContextCompositeNode.ports_setup(self)
 
     def conf_schema(self):
-        cache_key, task_graph, replacementObj = self._compute_hash_key()
-        if cache_key in CACHE_SCHEMA:
-            return CACHE_SCHEMA[cache_key]
+        task_graph = self.task_graph
+        # replacementObj = self.replacementObj
+        # # cache_key, task_graph, replacementObj = self._compute_has
+        # cache_key, task_graph, replacementObj = self._compute_hash_key()
+        # if cache_key in CACHE_SCHEMA:
+        #     return CACHE_SCHEMA[cache_key]
         # get's the input when it gets the conf
         input_meta = self.get_input_meta()
         json = {}
@@ -323,15 +581,19 @@ def conf_schema(self):
             if 'context' in conf:
                 json = deepcopy(_CONF_JSON)
                 metrics = []
-                task_graph.build(replace=replacementObj)
+                # task_graph.build(replace=replacementObj)
                 for t in task_graph:
                     node_id = t.get('id')
                     if node_id != '':
                         node = task_graph[node_id]
                         all_ports = node.ports_setup()
                         for port in all_ports.outports.keys():
-                            if all_ports.outports[port][
-                                    PortsSpecSchema.port_type] == float:
+                            types = all_ports.outports[port][
+                                PortsSpecSchema.port_type]
+                            if types == float:
+                                metrics.append(node_id+'.'+port)
+                            elif (isinstance(types, list)
+                                  and types[0] == float):
                                 metrics.append(node_id+'.'+port)
                 context = conf['context']
                 json['properties']['parameters'][
@@ -366,7 +628,7 @@ def conf_schema(self):
             }
         }
         out_schema = ConfSchema(json=json, ui=ui)
-        CACHE_SCHEMA[cache_key] = out_schema
+        # CACHE_SCHEMA[cache_key] = out_schema
         return out_schema
 
     def meta_setup(self):
@@ -391,8 +653,6 @@ def meta_setup(self):
         return out_meta
 
     def process(self, inputs):
-        import ray
-        from ray import tune
         if self.INPUT_CONFIG in inputs:
             self.conf.update(inputs[self.INPUT_CONFIG].data)
         output = {}
@@ -408,85 +668,6 @@ def process(self, inputs):
                     data_store[key] = ray.put(v)
             # here we need to do the hyper parameter search
 
-            def search_fun(config, checkpoint_dir=None):
-                myinputs = {}
-                for key in data_store.keys():
-                    v = ray.get(data_store[key])
-                    if isinstance(v, pandas.DataFrame):
-                        myinputs[key] = cudf.from_pandas(v)
-                    else:
-                        myinputs[key] = v
-                task_graph = TaskGraph.load_taskgraph(
-                    get_file_path(self.conf['taskgraph']))
-                task_graph.build()
-
-                outputLists = []
-                replaceObj = {}
-                input_feeders = []
-
-                def inputNode_fun(inputNode, in_ports):
-                    inports = inputNode.ports_setup().inports
-
-                    class InputFeed(Node):
-
-                        def meta_setup(self):
-                            output = {}
-                            for inp in inputNode.inputs:
-                                output[inp['to_port']] = inp[
-                                    'from_node'].meta_setup().outports[
-                                        inp['from_port']]
-                            # it will be something like { input_port: columns }
-                            return MetaData(inports={}, outports=output)
-
-                        def ports_setup(self):
-                            # it will be something like { input_port: types }
-                            return NodePorts(inports={}, outports=inports)
-
-                        def conf_schema(self):
-                            return ConfSchema()
-
-                        def process(self, empty):
-                            output = {}
-                            for key in inports.keys():
-                                if (inputNode.uid+'@'+key
-                                        in myinputs):
-                                    output[key] = myinputs[
-                                        inputNode.uid+'@'+key]
-                            return output
-
-                    uni_id = str(uuid.uuid1())
-                    obj = {
-                        TaskSpecSchema.task_id: uni_id,
-                        TaskSpecSchema.conf: {},
-                        TaskSpecSchema.node_type: InputFeed,
-                        TaskSpecSchema.inputs: []
-                    }
-                    input_feeders.append(obj)
-                    newInputs = {}
-                    for key in inports.keys():
-                        if inputNode.uid+'@'+key in myinputs:
-                            newInputs[key] = uni_id+'.'+key
-                    for inp in inputNode.inputs:
-                        if inp['to_port'] not in in_ports:
-                            # need to keep the old connections
-                            newInputs[inp['to_port']] = (
-                                inp['from_node'].uid + '.' + inp['from_port'])
-                    replaceObj.update({inputNode.uid: {
-                        TaskSpecSchema.inputs: newInputs}
-                    })
-
-                def outNode_fun(outNode, out_ports):
-                    pass
-                outputLists = self.conf['metrics']
-
-                self._make_sub_graph_connection(task_graph,
-                                                inputNode_fun, outNode_fun)
-
-                task_graph.extend(input_feeders)
-                self.update_conf_for_search(replaceObj, task_graph, config)
-                result = task_graph.run(outputLists, replace=replaceObj)
-                metric_report = {item: result[item] for item in outputLists}
-                tune.report(**metric_report)
             config = {}
             for para in self.conf['parameters']:
                 fun_name = para['search']['function']
@@ -495,7 +676,10 @@ def outNode_fun(outNode, out_ports):
                     config[para['name']] = fun(para['search']['args'])
                 else:
                     config[para['name']] = fun(*para['search']['args'])
-            analysis = tune.run(search_fun, **self.conf['tune'], config=config)
+            clean_inputs = get_clean_inputs(self.conf, self.task_graph,
+                                            self.inputs)
+            fun = get_search_fun(data_store, self.conf, clean_inputs)
+            analysis = tune.run(fun, **self.conf['tune'], config=config)
             best = analysis.get_best_config(**self.conf['best'])
             for key in best.keys():
                 self.conf['context'][key]['value'] = best[key]
@@ -503,26 +687,3 @@ def outNode_fun(outNode, out_ports):
         more_output = ContextCompositeNode.process(self, inputs)
         output.update(more_output)
         return output
-
-    def update_conf_for_search(self, replaceObj, task_graph, config):
-        # find the other replacment conf
-        if task_graph:
-            for task in task_graph:
-                key = task.get('id')
-                newid = key
-                conf = task.get('conf')
-                if newid in replaceObj:
-                    replaceObj[newid].update({'conf': conf})
-                else:
-                    replaceObj[newid] = {}
-                    replaceObj[newid].update({'conf': conf})
-        # replace the numbers from the context
-        if 'context' in self.conf:
-            for key in self.conf['context'].keys():
-                val = self.conf['context'][key]['value']
-                if key in config:
-                    val = config[key]
-                for map_obj in self.conf['context'][key]['map']:
-                    xpath = map_obj['xpath']
-                    expr = parse(xpath)
-                    expr.update(replaceObj, val)
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/splitDataNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/splitDataNode.py
index 2abbd42c..f8957d6d 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/splitDataNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/splitDataNode.py
@@ -1,85 +1,78 @@
-from .._port_type_node import _PortTypesMixin
-from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema, MetaData,
-                                                   NodePorts, PortsSpecSchema)
-from greenflow.dataframe_flow import Node
-import cudf
-import dask_cudf
 import cuml
 import copy
-
+from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema,
+                                                      PortsSpecSchema)
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow import Node
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 __all__ = ['DataSplittingNode']
 
 
-class DataSplittingNode(_PortTypesMixin, Node):
+class DataSplittingNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
         self.delayed_process = True
+        port_type = PortsSpecSchema.port_type
         self.INPUT_PORT_NAME = 'in'
         self.OUTPUT_PORT_NAME_TRAIN = 'train'
         self.OUTPUT_PORT_NAME_TEST = 'test'
-
-    def ports_setup_from_types(self, types):
-        port_type = PortsSpecSchema.port_type
-        input_ports = {
+        port_inports = {
             self.INPUT_PORT_NAME: {
-                port_type: types
-            }
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
         }
-
-        output_ports = {
+        port_outports = {
             self.OUTPUT_PORT_NAME_TRAIN: {
-                port_type: types
+                port_type: "${port:in}"
             },
             self.OUTPUT_PORT_NAME_TEST: {
-                port_type: types
+                port_type: "${port:in}"
             }
         }
-        input_connections = self.get_connected_inports()
-        if self.INPUT_PORT_NAME in input_connections:
-            determined_type = input_connections[self.INPUT_PORT_NAME]
-            # connected
-            return NodePorts(inports={self.INPUT_PORT_NAME: {
-                port_type: determined_type}},
-                outports={self.OUTPUT_PORT_NAME_TEST: {
-                    port_type: determined_type},
-                    self.OUTPUT_PORT_NAME_TRAIN: {
-                    port_type: determined_type}
-            })
-        else:
-            return NodePorts(inports=input_ports, outports=output_ports)
-
-    def ports_setup(self):
-        types = [cudf.DataFrame,
-                 dask_cudf.DataFrame]
-        return self.ports_setup_from_types(types)
-
-    def meta_setup(self):
-        cols_required = {}
-        col_from_inport = {}
-        required = {
-            self.INPUT_PORT_NAME: cols_required
+        meta_inports = {
+            self.INPUT_PORT_NAME: {}
         }
-        input_meta = self.get_input_meta()
-        if self.INPUT_PORT_NAME in input_meta:
-            col_inport = input_meta[self.INPUT_PORT_NAME]
-            if 'target' in self.conf:
-                target_col = self.conf['target']
-                for i in sorted(col_inport.keys()):
-                    if i != target_col:
-                        col_from_inport[i] = col_inport[i]
-                col_from_inport[target_col] = col_inport[target_col]
-                if target_col in col_inport:
-                    required[self.INPUT_PORT_NAME][target_col] = \
-                        col_inport[target_col]
-        else:
-            col_from_inport = required[self.INPUT_PORT_NAME]
-        output_cols = {
-            self.OUTPUT_PORT_NAME_TRAIN: col_from_inport,
-            self.OUTPUT_PORT_NAME_TEST: col_from_inport
+        meta_outports = {
+            self.OUTPUT_PORT_NAME_TRAIN: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_DELETION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: {}
+            },
+            self.OUTPUT_PORT_NAME_TEST: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_DELETION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: {}
+            }
         }
-        metadata = MetaData(inports=required, outports=output_cols)
-        return metadata
+        if 'target' in self.conf:
+            target_col = self.conf['target']
+            meta_inports = {
+                self.INPUT_PORT_NAME: {
+                    target_col: None
+                }
+            }
+            meta_outports[self.OUTPUT_PORT_NAME_TEST][
+                MetaDataSchema.META_ORDER] = {
+                target_col: -1
+            }
+            meta_outports[self.OUTPUT_PORT_NAME_TRAIN][
+                MetaDataSchema.META_ORDER] = {
+                    target_col: -1,
+                }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -129,7 +122,9 @@ def process(self, inputs):
         """
         input_df = inputs[self.INPUT_PORT_NAME]
         target_col = self.conf['target']
-        train_cols = input_df.columns.difference([target_col])
+        train_cols = list(input_df.columns)
+        if target_col in train_cols:
+            train_cols.remove(target_col)
         conf = copy.copy(self.conf)
         del conf['target']
         r = cuml.preprocessing.model_selection.train_test_split(
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/xgboostNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/xgboostNode.py
index 95add48c..d0ebcc83 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/xgboostNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/ml/xgboostNode.py
@@ -3,60 +3,68 @@
 import dask_cudf
 import xgboost as xgb
 import dask
-from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema, MetaData,
-                                                      PortsSpecSchema,
-                                                      NodePorts)
-from xgboost import Booster
+from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema,
+                                                      PortsSpecSchema)
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
 import copy
 from collections import OrderedDict
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 
 __all__ = ['TrainXGBoostNode', 'InferXGBoostNode']
 
 
-class TrainXGBoostNode(_PortTypesMixin, Node):
+class TrainXGBoostNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'in'
         self.OUTPUT_PORT_NAME = 'model_out'
-
-    def ports_setup_from_types(self, types):
         port_type = PortsSpecSchema.port_type
-        input_ports = {
+        port_inports = {
             self.INPUT_PORT_NAME: {
-                port_type: types
+                port_type: [
+                    "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
             }
         }
-        output_ports = {
+        port_outports = {
             self.OUTPUT_PORT_NAME: {
-                port_type: [Booster, dict]
+                port_type: ['xgboost.Booster', 'builtins.dict']
             }
         }
-        input_connections = self.get_connected_inports()
-        if self.INPUT_PORT_NAME in input_connections:
-            determined_type = input_connections[self.INPUT_PORT_NAME]
-            input_ports.update({self.INPUT_PORT_NAME:
-                                {port_type: determined_type}})
-            return NodePorts(inports=input_ports,
-                             outports=output_ports)
-        else:
-            return NodePorts(inports=input_ports, outports=output_ports)
 
-    def meta_setup(self):
         cols_required = {}
-        required = {
-            self.INPUT_PORT_NAME: cols_required
-        }
         if 'columns' in self.conf and self.conf.get('include', True):
             cols_required = {}
             for col in self.conf['columns']:
                 cols_required[col] = None
-            required = {
-                self.INPUT_PORT_NAME: cols_required
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required,
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: {}
             }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
+
+    def update(self):
+        TemplateNodeMixin.update(self)
         input_meta = self.get_input_meta()
         if self.INPUT_PORT_NAME in input_meta:
+            meta_inports = self.template_meta_setup().inports
+            meta_outports = self.template_meta_setup().outports
             col_from_inport = input_meta[self.INPUT_PORT_NAME]
             enums = [col for col in col_from_inport.keys()]
             cols_output = {}
@@ -87,26 +95,13 @@ def meta_setup(self):
                     cols_required[self.conf['target']] = None
                     cols_output['label'][
                         self.conf['target']] = None
-                required = {
-                    self.INPUT_PORT_NAME: cols_required,
-                }
-            output_cols = {
-                self.OUTPUT_PORT_NAME: cols_output,
-            }
-            metadata = MetaData(inports=required, outports=output_cols)
-            return metadata
-        else:
-            col_from_inport = {}
-            output_cols = {
-                self.OUTPUT_PORT_NAME: col_from_inport,
-            }
-            metadata = MetaData(inports=required, outports=output_cols)
-            return metadata
-
-    def ports_setup(self):
-        types = [cudf.DataFrame,
-                 dask_cudf.DataFrame]
-        return self.ports_setup_from_types(types)
+                meta_inports[self.INPUT_PORT_NAME] = cols_required
+            meta_outports[self.OUTPUT_PORT_NAME][
+                MetaDataSchema.META_DATA] = cols_output
+            self.template_meta_setup(
+                in_ports=meta_inports,
+                out_ports=meta_outports
+            )
 
     def conf_schema(self):
         json = {
@@ -343,50 +338,59 @@ def process(self, inputs):
         return {self.OUTPUT_PORT_NAME: bst}
 
 
-class InferXGBoostNode(Node):
+class InferXGBoostNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'data_in'
         self.INPUT_PORT_MODEL_NAME = 'model_in'
         self.OUTPUT_PORT_NAME = 'out'
-
-    def ports_setup_from_types(self, types):
         port_type = PortsSpecSchema.port_type
-        input_ports = {
+        port_inports = {
             self.INPUT_PORT_NAME: {
-                port_type: types
+                port_type: [
+                    "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
             },
             self.INPUT_PORT_MODEL_NAME: {
-                port_type: [Booster, dict]
+                port_type: ['xgboost.Booster', 'builtins.dict']
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:data_in}"
             }
         }
-        output_ports = {
+
+        meta_inports = {
+            self.INPUT_PORT_NAME: {},
+            self.INPUT_PORT_MODEL_NAME: {}
+        }
+        predict = self.conf.get('prediction', 'predict')
+        out_cols = {predict: None}
+        meta_outports = {
             self.OUTPUT_PORT_NAME: {
-                port_type: types
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: out_cols
             }
         }
-        input_connections = self.get_connected_inports()
-        if self.INPUT_PORT_NAME in input_connections:
-            determined_type = input_connections[self.INPUT_PORT_NAME]
-            input_ports.update({self.INPUT_PORT_NAME:
-                                {port_type: determined_type}})
-            output_ports.update({self.OUTPUT_PORT_NAME:
-                                {port_type: determined_type}})
-            return NodePorts(inports=input_ports,
-                             outports=output_ports)
-        else:
-            return NodePorts(inports=input_ports, outports=output_ports)
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
-    def meta_setup(self):
-        metadata = MetaData()
+    def update(self):
+        TemplateNodeMixin.update(self)
         input_meta = self.get_input_meta()
-        required = {self.INPUT_PORT_NAME: {},
-                    self.INPUT_PORT_MODEL_NAME: {}}
         predict = self.conf.get('prediction', 'predict')
         pred_contribs: bool = self.conf.get('pred_contribs', False)
-        output_cols = {
-            self.OUTPUT_PORT_NAME: {predict: None}
-        }
+        meta_inports = self.template_meta_setup().inports
+        meta_outports = self.template_meta_setup().outports
         if (self.INPUT_PORT_NAME in input_meta
                 and self.INPUT_PORT_MODEL_NAME in input_meta):
             col_from_inport = input_meta[self.INPUT_PORT_NAME]
@@ -395,20 +399,15 @@ def meta_setup(self):
                     self.INPUT_PORT_MODEL_NAME]['train']
             else:
                 required_cols = {}
-            predict = self.conf.get('prediction', 'predict')
             if not pred_contribs:
                 col_from_inport[predict] = None  # the type is not determined
             else:
                 col_from_inport = {}
                 for i in range(len(required_cols)+1):
                     col_from_inport[i] = None
-            required = {self.INPUT_PORT_NAME: required_cols,
-                        self.INPUT_PORT_MODEL_NAME: {}}
-            output_cols = {
-                self.OUTPUT_PORT_NAME: col_from_inport,
-            }
-            metadata = MetaData(inports=required, outports=output_cols)
-            return metadata
+            meta_inports[self.INPUT_PORT_NAME] = required_cols
+            meta_outports[self.OUTPUT_PORT_NAME][
+                MetaDataSchema.META_DATA] = col_from_inport
         elif (self.INPUT_PORT_NAME not in input_meta and
               self.INPUT_PORT_MODEL_NAME in input_meta):
             if 'train' in input_meta[self.INPUT_PORT_MODEL_NAME]:
@@ -416,7 +415,6 @@ def meta_setup(self):
                     self.INPUT_PORT_MODEL_NAME]['train']
             else:
                 required_cols = {}
-            predict = self.conf.get('prediction', 'predict')
             col_from_inport = copy.copy(required_cols)
             if not pred_contribs:
                 col_from_inport[predict] = None  # the type is not determined
@@ -424,31 +422,20 @@ def meta_setup(self):
                 col_from_inport = {}
                 for i in range(len(required_cols)+1):
                     col_from_inport[i] = None
-            required = {self.INPUT_PORT_NAME: required_cols,
-                        self.INPUT_PORT_MODEL_NAME: {}}
-            output_cols = {
-                self.OUTPUT_PORT_NAME: col_from_inport
-            }
-            metadata = MetaData(inports=required, outports=output_cols)
-            return metadata
+            meta_inports[self.INPUT_PORT_NAME] = required_cols
+            meta_outports[self.OUTPUT_PORT_NAME][
+                MetaDataSchema.META_DATA] = col_from_inport
         elif (self.INPUT_PORT_NAME in input_meta and
               self.INPUT_PORT_MODEL_NAME not in input_meta):
             col_from_inport = input_meta[self.INPUT_PORT_NAME]
-            predict = self.conf.get('prediction', 'predict')
             if not pred_contribs:
                 col_from_inport[predict] = None  # the type is not determined
-            output_cols = {
-                self.OUTPUT_PORT_NAME: col_from_inport
-            }
-            metadata = MetaData(inports=required, outports=output_cols)
-            return metadata
-        metadata = MetaData(inports=required, outports=output_cols)
-        return metadata
-
-    def ports_setup(self):
-        types = [cudf.DataFrame,
-                 dask_cudf.DataFrame]
-        return self.ports_setup_from_types(types)
+            meta_outports[self.OUTPUT_PORT_NAME][
+                MetaDataSchema.META_DATA] = col_from_inport
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/node_hdf_cache.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/node_hdf_cache.py
new file mode 100644
index 00000000..c25eba1e
--- /dev/null
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/node_hdf_cache.py
@@ -0,0 +1,81 @@
+import cudf
+import os
+import warnings
+import pandas as pd
+
+from greenflow.dataframe_flow.portsSpecSchema import PortsSpecSchema
+
+__all__ = ['NodeHDFCacheMixin']
+
+
+class NodeHDFCacheMixin:
+    def load_cache(self, filename=None) -> dict:
+        """
+        Defines the behavior of how to load the cache file from the `filename`.
+        Node can override this method. Default implementation assumes cudf
+        dataframes.
+
+        Arguments
+        -------
+        filename: str
+            filename of the cache file. Leave as none to use default.
+        returns: dict
+            dictionary of the output from this node
+        """
+        cache_dir = os.getenv('GREENFLOW_CACHE_DIR', self.cache_dir)
+        if filename is None:
+            filename = cache_dir + '/' + self.uid + '.hdf5'
+
+        output_df = {}
+        with pd.HDFStore(filename, mode='r') as hf:
+            for oport, pspec in \
+                    self._get_output_ports(full_port_spec=True).items():
+                ptype = pspec.get(PortsSpecSchema.port_type)
+                if self.outport_connected(oport):
+                    ptype = ([ptype] if not isinstance(ptype,
+                                                       list) else ptype)
+                    key = '{}/{}'.format(self.uid, oport)
+                    # check hdf store for the key
+                    if key not in hf:
+                        raise Exception(
+                            'The task "{}" port "{}" key "{}" not found in'
+                            'the hdf file "{}". Cannot load from cache.'
+                            .format(self.uid, oport, key, filename)
+                        )
+                    if cudf.DataFrame not in ptype:
+                        warnings.warn(
+                            RuntimeWarning,
+                            'Task "{}" port "{}" port type is not set to '
+                            'cudf.DataFrame. Attempting to load port data '
+                            'with cudf.read_hdf.'.format(self.uid, oport))
+                    output_df[oport] = cudf.read_hdf(hf, key)
+        return output_df
+
+    def save_cache(self, output_data: dict):
+        '''Defines the behavior for how to save the output of a node to
+        filesystem cache. Default implementation assumes cudf dataframes.
+
+        :param output_data: The output from :meth:`process`. For saving to hdf
+            requires that the dataframe(s) have `to_hdf` method.
+        '''
+        cache_dir = os.getenv('GREENFLOW_CACHE_DIR', self.cache_dir)
+        os.makedirs(cache_dir, exist_ok=True)
+        filename = cache_dir + '/' + self.uid + '.hdf5'
+        with pd.HDFStore(filename, mode='w') as hf:
+            for oport, odf in output_data.items():
+                # check for to_hdf attribute
+                if not hasattr(odf, 'to_hdf'):
+                    raise Exception(
+                        'Task "{}" port "{}" output object is missing '
+                        '"to_hdf" attribute. Cannot save to cache.'
+                        .format(self.uid, oport))
+
+                dtype = '{}'.format(type(odf)).lower()
+                if 'dataframe' not in dtype:
+                    warnings.warn(
+                        RuntimeWarning,
+                        'Task "{}" port "{}" port type is not a dataframe.'
+                        ' Attempting to save to hdf with "to_hdf" method.'
+                        .format(self.uid, oport))
+                key = '{}/{}'.format(self.uid, oport)
+                odf.to_hdf(hf, key, format='table', data_columns=True)
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/portofolio/simpleAveragePortOpt.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/portofolio/simpleAveragePortOpt.py
index 9ee3e19a..9a1ab328 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/portofolio/simpleAveragePortOpt.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/portofolio/simpleAveragePortOpt.py
@@ -1,27 +1,52 @@
-from greenflow.dataframe_flow import Node
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow import Node, PortsSpecSchema
 from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 
-class SimpleAveragePortOpt(_PortTypesMixin, Node):
+class SimpleAveragePortOpt(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'stock_in'
         self.OUTPUT_PORT_NAME = 'stock_out'
-
-    def meta_setup(self):
-        cols_required = {"datetime": "date",
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:stock_in}"
+            }
+        }
+        cols_required = {"datetime": "datetime64[ns]",
                          "strategy_returns": "float64",
                          "asset": "int64"}
-        retention = {"datetime": "date",
+        retention = {"datetime": "datetime64[ns]",
                      "strategy_returns": "float64"}
-        return _PortTypesMixin.retention_meta_setup(self,
-                                                    retention,
-                                                    required=cols_required)
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: retention
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -48,6 +73,6 @@ def process(self, inputs):
         """
         input_df = inputs[self.INPUT_PORT_NAME]
         port = input_df[['datetime', 'strategy_returns']] \
-            .groupby(['datetime']).mean().reset_index()
+            .groupby(['datetime']).mean().reset_index().sort_values('datetime')
         port.columns = ['datetime', 'strategy_returns']
         return {self.OUTPUT_PORT_NAME: port}
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/strategy/movingAverageStrategyNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/strategy/movingAverageStrategyNode.py
index d8451e1d..ff4d4675 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/strategy/movingAverageStrategyNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/strategy/movingAverageStrategyNode.py
@@ -1,12 +1,13 @@
 from .. import cuindicator as ci
-from greenflow.dataframe_flow import Node
+from greenflow.dataframe_flow import Node, PortsSpecSchema
 from numba import cuda
 import math
 import numpy as np
 import cudf
-import dask_cudf
-from .._port_type_node import _PortTypesMixin
 from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 
 @cuda.jit
@@ -42,27 +43,49 @@ def moving_average_signal(stock_df, n_fast, n_slow):
     return out_arr, ma_slow, ma_fast
 
 
-class MovingAverageStrategyNode(_PortTypesMixin, Node):
+class MovingAverageStrategyNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'stock_in'
         self.OUTPUT_PORT_NAME = 'stock_out'
         self.delayed_process = True
-
-    def meta_setup(self):
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:stock_in}"
+            }
+        }
         cols_required = {"close": "float64"}
         addition = {"signal": "float64",
                     "ma_slow": "float64",
                     "ma_fast": "float64"}
-        return _PortTypesMixin.addition_meta_setup(self,
-                                                   addition,
-                                                   required=cols_required)
-
-    def ports_setup(self):
-        types = [cudf.DataFrame,
-                 dask_cudf.DataFrame]
-        return _PortTypesMixin.ports_setup_from_types(self, types)
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: addition
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/strategy/portExpMovingAverageStrategyNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/strategy/portExpMovingAverageStrategyNode.py
index 162d386e..e443dd9f 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/strategy/portExpMovingAverageStrategyNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/strategy/portExpMovingAverageStrategyNode.py
@@ -1,8 +1,3 @@
-from greenflow.dataframe_flow import Node
-from .._port_type_node import _PortTypesMixin
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
-from .. import cuindicator as ci
-import dask_cudf
 from numba import cuda
 from functools import partial
 import math
@@ -10,6 +5,14 @@
 import cudf
 import pandas as pd
 
+from greenflow.dataframe_flow import Node, PortsSpecSchema
+from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
+
+from .. import cuindicator as ci
+
 
 @cuda.jit
 def moving_average_signal_kernel(ma_fast, ma_slow, out_arr, arr_len):
@@ -46,29 +49,61 @@ def port_exponential_moving_average(stock_df, n_fast, n_slow):
     return out_arr, ma_slow, ma_fast
 
 
-class PortExpMovingAverageStrategyNode(_PortTypesMixin, Node):
+def cpu_exp_moving_average(df, n_slow=1, n_fast=3):
+    df['exp_ma_slow'] = df['close'].ewm(span=n_slow, min_periods=n_slow).mean()
+    df['exp_ma_fast'] = df['close'].ewm(span=n_fast, min_periods=n_fast).mean()
+    df['signal'] = 1.0
+    df['signal'][df['exp_ma_fast'] - df['exp_ma_slow'] > 0.00001] = -1.0
+    df['signal'] = df['signal'].shift(1)
+    df['signal'].values[0:n_slow] = np.nan
+    return df
+
+
+class PortExpMovingAverageStrategyNode(
+        TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'stock_in'
         self.OUTPUT_PORT_NAME = 'stock_out'
         self.delayed_process = True
-
-    def ports_setup(self):
-        types = [cudf.DataFrame,
-                 dask_cudf.DataFrame,
-                 pd.DataFrame]
-        return _PortTypesMixin.ports_setup_from_types(self, types)
-
-    def meta_setup(self):
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:stock_in}"
+            }
+        }
         cols_required = {"close": "float64",
                          "indicator": "int32"}
         addition = {"signal": "float64",
                     "exp_ma_slow": "float64",
                     "exp_ma_fast": "float64"}
-        return _PortTypesMixin.addition_meta_setup(self,
-                                                   addition,
-                                                   required=cols_required)
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: addition
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -137,13 +172,3 @@ def process(self, inputs):
             input_df = input_df.dropna()
             input_df = input_df.query('indicator == 0')
         return {self.OUTPUT_PORT_NAME: input_df}
-
-
-def cpu_exp_moving_average(df, n_slow=1, n_fast=3):
-    df['exp_ma_slow'] = df['close'].ewm(span=n_slow, min_periods=n_slow).mean()
-    df['exp_ma_fast'] = df['close'].ewm(span=n_fast, min_periods=n_fast).mean()
-    df['signal'] = 1.0
-    df['signal'][df['exp_ma_fast'] - df['exp_ma_slow'] > 0.00001] = -1.0
-    df['signal'] = df['signal'].shift(1)
-    df['signal'].values[0:n_slow] = np.nan
-    return df
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/strategy/xgboostStrategyNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/strategy/xgboostStrategyNode.py
index 35ee7bca..a8c9f340 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/strategy/xgboostStrategyNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/strategy/xgboostStrategyNode.py
@@ -1,244 +1,79 @@
-from greenflow.dataframe_flow import Node
-import datetime
-import cudf
-import dask_cudf
-import xgboost as xgb
-import dask
-from .._port_type_node import _PortTypesMixin
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema, MetaData
-
+from greenflow.dataframe_flow import Node, PortsSpecSchema
+from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 __all__ = ['XGBoostStrategyNode']
 
 
-class XGBoostStrategyNode(_PortTypesMixin, Node):
+class XGBoostStrategyNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
     """
     This is the Node used to compute trading signal from XGBoost Strategy.
-    It requires the following conf fields:
-        "train_date": a date string of "Y-m-d" format. All the data points
-        before this date is considered as training, otherwise as testing. If
-        not provided, all the data points are considered as training.
-        "xgboost_parameters": a dictionary of any legal parameters for XGBoost
-        models. It overwrites the default parameters used in the process method
-        "no_feature": specifying a list of columns in the input dataframe that
-        should NOT be considered as training features.
-        "target": the column that is considered as "target" in machine learning
-        algorithm
-    It requires the "datetime" column for spliting the data points and adds a
-    new column "signal" to be used for backtesting.
-    The detailed computation steps are listed in the process method's docstring
+
     """
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'stock_in'
         self.OUTPUT_PORT_NAME = 'stock_out'
-
-    def meta_setup(self):
-        # if 'no_feature' in self.conf:
-        #     retention = self.conf['no_feature']
-        # else:
-        cols_required = {'datetime': 'date',
-                         "asset": "int64"}
-        # self.delayed_process = True
-        required = {
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:stock_in}"
+            }
+        }
+        cols_required = {'predict': None, "asset": "int64"}
+        addition = {}
+        addition['signal'] = 'float64'
+        meta_inports = {
             self.INPUT_PORT_NAME: cols_required
         }
-        retention = {}
-        retention['signal'] = 'float64'
-        # _PortTypesMixin.retention_meta_setup(self, retention)
-
-        input_meta = self.get_input_meta()
-        if self.INPUT_PORT_NAME not in input_meta:
-            col_from_inport = required[self.INPUT_PORT_NAME]
-        else:
-            col_from_inport = input_meta[self.INPUT_PORT_NAME]
-        # delete the columns from the inputs
-        if 'no_feature' in self.conf:
-            for key in self.conf['no_feature']:
-                if key in col_from_inport:
-                    retention[key] = col_from_inport[key]
-        metadata = MetaData(inports=required,
-                            outports={self.OUTPUT_PORT_NAME: retention})
-        return metadata
-
-    def ports_setup(self):
-        types = [cudf.DataFrame,
-                 dask_cudf.DataFrame]
-        return _PortTypesMixin.ports_setup_from_types(self, types)
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: addition
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
             "title": "XGBoost Node configure",
             "type": "object",
-            "description": """Split the data into training and testing based on
-             'train_data', train a XGBoost model based on the training data,
-             make predictions for all the data points, compute the trading.
+            "description": """convert the predicted next day return as trading actions
             """,
             "properties": {
-                "num_of_rounds": {
-                    "type": "number",
-                    "description": """The number of rounds for boosting""",
-                    "default": 100
-                },
-                "train_date":  {
-                    "type": "string",
-                    "description": """the date to splite train and validation
-                    dataset"""
-                },
-                "target":  {
-                    "type": "string",
-                    "description": "the column used as dependent variable"
-                },
-                "no_feature": {
-                    "type": "array",
-                    "items": {
-                        "type": "string",
-                    },
-                    "description": """columns in the input dataframe that
-        should NOT be considered as training features."""
-                },
-                "xgboost_parameters": {
-                    "type": "object",
-                    "description": "xgoobst parameters",
-                    "properties": {
-                        'max_depth': {
-                            "type": "number",
-                            "description": "Maximum depth of a tree.",
-                            "default": 8
-                        },
-                        "max_leaves": {
-                            "type": "number",
-                            "description": "maximum number of tree leaves",
-                            "default": 2**8
-                        },
-                        "gamma": {
-                            "type": "number",
-                            "description": """Minimum loss reduction required
-                            to make a further partition on a leaf node of the
-                            tree.""",
-                            "default": 0
-                        },
-                        "objective": {
-                            "type": "string",
-                            "enum": ["reg:squarederror", "reg:squaredlogerror",
-                                     "reg:logistic", "reg:pseudohubererror"],
-                            "description": """Specify the learning task and
-                            the corresponding learning objective.""",
-                            "default": "reg:squarederror"
-                        }
-                    }
-                }
             },
-            "required": ["target", "num_of_rounds"],
         }
         ui = {
-            "train_date":  {
-                "ui:widget": "alt-date",
-                "ui:options": {
-                        "yearsRange": [1985, 2025],
-                        "hideNowButton": True,
-                        "hideClearButton": True,
-                },
-            },
         }
-        input_meta = self.get_input_meta()
-        if self.INPUT_PORT_NAME in input_meta:
-            col_from_inport = input_meta[self.INPUT_PORT_NAME]
-            enums = [col for col in col_from_inport.keys()]
-            json['properties']['no_feature']['items']['enum'] = enums
-            json['properties']['target']['enum'] = enums
-            return ConfSchema(json=json, ui=ui)
-        else:
-            return ConfSchema(json=json, ui=ui)
+        return ConfSchema(json=json, ui=ui)
 
     def process(self, inputs):
-        """
-        The process is doing following things:
-            1. split the data into training and testing based on provided
-               conf['train_date']. If it is not provided, all the data is
-               treated as training data.
-            2. train a XGBoost model based on the training data
-            3. Make predictions for all the data points including training and
-               testing.
-            4. From the prediction of returns, compute the trading signals that
-               can be used in the backtesting.
-        Arguments
-        -------
-         inputs: list
-            list of input dataframes.
-        Returns
-        -------
-        dataframe
-        """
-        dxgb_params = {
-                'max_depth':         8,
-                'max_leaves':        2 ** 8,
-                'tree_method':       'gpu_hist',
-                'objective':         'reg:squarederror',
-                'grow_policy':       'lossguide',
-        }
-        # num_of_rounds = 100
-        if 'xgboost_parameters' in self.conf:
-            dxgb_params.update(self.conf['xgboost_parameters'])
         input_df = inputs[self.INPUT_PORT_NAME]
-        model_df = input_df
-        train_cols = set(model_df.columns) - set(
-            self.conf['no_feature'])
-        train_cols = list(train_cols - set([self.conf['target']]))
-
-        if isinstance(input_df, dask_cudf.DataFrame):
-            # get the client
-            client = dask.distributed.client.default_client()
-            if 'train_date' in self.conf:
-                train_date = datetime.datetime.strptime(self.conf['train_date'],  # noqa: F841, E501
-                                                        '%Y-%m-%d')
-                model_df = model_df[model_df.datetime < train_date]
-            train = model_df[train_cols]
-            target = model_df[self.conf['target']]
-            dmatrix = xgb.dask.DaskDMatrix(client, train, label=target)
-            bst = xgb.dask.train(client, dxgb_params, dmatrix,
-                                 num_boost_round=self.conf["num_of_rounds"])
-
-            dtrain = xgb.dask.DaskDMatrix(client, input_df[train_cols])
-            prediction = xgb.dask.predict(client, bst, dtrain)
-            pred_df = dask_cudf.from_dask_dataframe(
-                prediction.to_dask_dataframe())
-            pred_df.index = input_df.index
-            input_df['signal'] = pred_df
-        elif isinstance(input_df, cudf.DataFrame):
-            if 'train_date' in self.conf:
-                train_date = datetime.datetime.strptime(self.conf['train_date'],  # noqa: F841, E501
-                                                        '%Y-%m-%d')
-                model_df = model_df.query('datetime<@train_date')
-            train = model_df[train_cols]
-            target = model_df[self.conf['target']]
-            dmatrix = xgb.DMatrix(train, label=target)
-            bst = xgb.train(dxgb_params, dmatrix,
-                            num_boost_round=self.conf["num_of_rounds"])
-            infer_dmatrix = xgb.DMatrix(input_df[train_cols])
-            prediction = cudf.Series(bst.predict(infer_dmatrix),
-                                     nan_as_null=False,
-                                     index=input_df.index
-                                     ).astype('float64')
-            input_df['signal'] = prediction
-
-        input_df['tmp'] = (input_df['asset'] -
-                           input_df['asset'].shift(1)).fillna(1)
-        input_df['tmp'] = (input_df['tmp'] != 0).astype('int32')
-        tmp = input_df['tmp']
-        input_df['tmp'] = tmp.where(tmp != 1, None)
-        input_df = input_df.dropna(subset=['tmp'])
-        input_df = input_df.drop('tmp', axis=1)
-
         # convert the signal to trading action
         # 1 is buy and -1 is sell
         # It predicts the tomorrow's return (shift -1)
         # We shift 1 for trading actions so that it acts on the second day
         input_df['signal'] = ((
-            input_df['signal'] >= 0).astype('float') * 2 - 1).shift(1)
-
+            input_df['predict'] >= 0).astype('float') * 2 - 1).shift(1)
         # remove the bad datapints
         input_df = input_df.dropna()
-        remaining = list(self.conf['no_feature']) + ['signal']
-        return {self.OUTPUT_PORT_NAME: input_df[remaining]}
+        return {self.OUTPUT_PORT_NAME: input_df}
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/__init__.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/__init__.py
index f2e88fc5..aa4a0d78 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/__init__.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/__init__.py
@@ -15,8 +15,6 @@
 from .addSignIndicator import AddSignIndicatorNode
 from .linearEmbedding import LinearEmbeddingNode
 from .onehotEncoding import OneHotEncodingNode
-from .daskComputeNode import DaskComputeNode
-from .persistNode import PersistNode
 
 __all__ = ["AverageNode", "AssetFilterNode", "LeftMergeNode",
            "ReturnFeatureNode", "SortNode",
@@ -24,4 +22,4 @@
            "ValueFilterNode", "RenameNode", "AssetIndicatorNode",
            "DropNode", "IndicatorNode", "NormalizationNode",
            "AddSignIndicatorNode", "LinearEmbeddingNode",
-           "OneHotEncodingNode", "DaskComputeNode", "PersistNode"]
+           "OneHotEncodingNode"]
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/addSignIndicator.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/addSignIndicator.py
index 0b5a2a43..3cdc88eb 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/addSignIndicator.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/addSignIndicator.py
@@ -1,19 +1,53 @@
-from greenflow.dataframe_flow import Node
+from greenflow.dataframe_flow import Node, PortsSpecSchema
 from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 __all__ = ["AddSignIndicatorNode"]
 
 
-class AddSignIndicatorNode(_PortTypesMixin, Node):
+class AddSignIndicatorNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'in'
         self.OUTPUT_PORT_NAME = 'out'
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:in}"
+            }
+        }
+        name = self.conf.get('sign', 'sign')
+        addition = {name: "int64"}
+        cols_required = {}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: addition
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -64,11 +98,3 @@ def process(self, inputs):
         name = self.conf.get('sign', 'sign')
         input_df[name] = (input_df[self.conf['column']] > 0).astype('int64')
         return {self.OUTPUT_PORT_NAME: input_df}
-
-    def meta_setup(self):
-        name = self.conf.get('sign', 'sign')
-        addition = {name: "int64"}
-        cols_required = {}
-        return _PortTypesMixin.addition_meta_setup(self,
-                                                   addition,
-                                                   required=cols_required)
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/assetFilterNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/assetFilterNode.py
index 7af7400a..9c85bfb3 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/assetFilterNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/assetFilterNode.py
@@ -1,52 +1,80 @@
 from greenflow.dataframe_flow import Node
-from .._port_type_node import _PortTypesMixin
 from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema,
-                                                   MetaData,
-                                                   PortsSpecSchema, NodePorts)
-from ..dataloader.stockMap import StockMap
+                                                      PortsSpecSchema)
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
+__all__ = ["AssetFilterNode"]
 
-class AssetFilterNode(_PortTypesMixin, Node):
+
+class AssetFilterNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'stock_in'
         self.OUTPUT_PORT_NAME = 'stock_out'
         self.INPUT_MAP_NAME = 'name_map'
         self.OUTPUT_ASSET_NAME = 'stock_name'
 
-    def ports_setup_from_types(self, types):
         port_type = PortsSpecSchema.port_type
-        input_ports = {
+        port_inports = {
             self.INPUT_PORT_NAME: {
-                port_type: types
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
             },
             self.INPUT_MAP_NAME: {
-                port_type: StockMap
+                port_type: [
+                    "greenflow_gquant_plugin.dataloader.stockMap.StockMap"
+                ]
             }
         }
-
-        output_ports = {
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:stock_in}"
+            },
+            self.OUTPUT_ASSET_NAME: {
+                port_type: ['builtins.str']
+            }
+        }
+        cols_required = {"asset": "int64"}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required,
+            self.INPUT_MAP_NAME: {}
+        }
+        meta_outports = {
             self.OUTPUT_PORT_NAME: {
-                port_type: types
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: {}
             },
             self.OUTPUT_ASSET_NAME: {
-                port_type: str
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: {}
             }
         }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
-        input_connections = self.get_connected_inports()
-        if self.INPUT_PORT_NAME in input_connections:
-            determined_type = input_connections[self.INPUT_PORT_NAME]
-            input_ports.update({self.INPUT_PORT_NAME:
-                                {port_type: determined_type}})
-            output_ports.update({self.OUTPUT_PORT_NAME: {
-                                 port_type: determined_type}})
-            # connected
-            return NodePorts(inports=input_ports,
-                             outports=output_ports)
-        else:
-            return NodePorts(inports=input_ports, outports=output_ports)
+    def update(self):
+        TemplateNodeMixin.update(self)
+        name = self._find_asset_name()
+        asset_retension = {"asset_name": name}
+        meta_outports = self.template_meta_setup().outports
+        meta_outports[self.OUTPUT_ASSET_NAME][
+            MetaDataSchema.META_DATA] = asset_retension
+        self.template_meta_setup(
+            in_ports=None,
+            out_ports=meta_outports
+        )
 
     def _find_asset_name(self):
         name = ""
@@ -65,33 +93,6 @@ def _find_asset_name(self):
                     name = ""
         return name
 
-    def meta_setup(self):
-        cols_required = {"asset": "int64"}
-        required = {
-            self.INPUT_PORT_NAME: cols_required
-        }
-        input_meta = self.get_input_meta()
-        name = self._find_asset_name()
-        if self.INPUT_PORT_NAME in input_meta:
-            col_from_inport = input_meta[self.INPUT_PORT_NAME]
-            output_cols = {
-                self.OUTPUT_PORT_NAME: col_from_inport,
-                self.OUTPUT_ASSET_NAME: {"asset_name": name}
-            }
-            metadata = MetaData(inports=required, outports=output_cols)
-            return metadata
-        else:
-            col_from_inport = required[self.INPUT_PORT_NAME]
-            output_cols = {
-                self.OUTPUT_PORT_NAME: col_from_inport,
-                self.OUTPUT_ASSET_NAME: {"asset_name": name}
-            }
-            metadata = MetaData(inports=required, outports=output_cols)
-            return metadata
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
-
     def conf_schema(self):
         json = {
             "title": "Asset Filter Node configure",
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/assetIndicatorNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/assetIndicatorNode.py
index 406c5ca2..0c879a80 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/assetIndicatorNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/assetIndicatorNode.py
@@ -1,24 +1,53 @@
-from greenflow.dataframe_flow import Node
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow import Node, PortsSpecSchema
 from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
+__all__ = ['AssetIndicatorNode']
 
-class AssetIndicatorNode(_PortTypesMixin, Node):
+
+class AssetIndicatorNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
         self.delayed_process = True
-        _PortTypesMixin.init(self)
         self.INPUT_PORT_NAME = 'stock_in'
         self.OUTPUT_PORT_NAME = 'stock_out'
-
-    def meta_setup(self):
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:stock_in}"
+            }
+        }
         cols_required = {"asset": "int64"}
-        return _PortTypesMixin.addition_meta_setup(self,
-                                                   {"indicator": "int32"},
-                                                   required=cols_required)
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
+        addition = {"indicator": "int32"}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: addition
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/averageNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/averageNode.py
index e0af909e..7bee6b56 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/averageNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/averageNode.py
@@ -1,14 +1,57 @@
-from greenflow.dataframe_flow import Node
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow import Node, PortsSpecSchema
 from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
+__all__ = ['AverageNode']
 
-class AverageNode(_PortTypesMixin, Node):
+
+class AverageNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'stock_in'
         self.OUTPUT_PORT_NAME = 'stock_out'
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:stock_in}"
+            }
+        }
+        cols_required = {"asset": "int64"}
+        if 'column' in self.conf:
+            retention = {
+                "asset": "int64",
+                self.conf['column']: "float64",
+            }
+        else:
+            retention = {"asset": "int64"}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: retention
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         input_meta = self.get_input_meta()
@@ -58,20 +101,3 @@ def process(self, inputs):
             .groupby(["asset"]).mean().reset_index()
         volume_df.columns = ['asset', average_column]
         return {self.OUTPUT_PORT_NAME: volume_df}
-
-    def meta_setup(self):
-        required_col = {"asset": "int64"}
-        if 'column' in self.conf:
-            retention = {self.conf['column']: "float64",
-                         "asset": "int64"}
-            return _PortTypesMixin.retention_meta_setup(self,
-                                                        retention,
-                                                        required=required_col)
-        else:
-            retention = {"asset": "int64"}
-            return _PortTypesMixin.retention_meta_setup(self,
-                                                        retention,
-                                                        required=required_col)
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/daskComputeNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/daskComputeNode.py
deleted file mode 100644
index a047e81e..00000000
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/daskComputeNode.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import pandas as pd
-import cudf
-from dask.dataframe import DataFrame as DaskDataFrame
-import dask_cudf
-
-from greenflow.dataframe_flow import Node
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
-from .._port_type_node import _PortTypesMixin
-
-from greenflow.dataframe_flow.portsSpecSchema import (
-    PortsSpecSchema, NodePorts)
-
-__all__ = ["DaskComputeNode"]
-
-
-class DaskComputeNode(_PortTypesMixin, Node):
-
-    def init(self):
-        _PortTypesMixin.init(self)
-        self.INPUT_PORT_NAME = 'in'
-        self.OUTPUT_PORT_NAME = 'out'
-
-    def conf_schema(self):
-        json = {
-            "title": "Run dask compute",
-            "type": "object",
-            "description": "If the input is a dask or dask_cudf dataframe "
-            "then run compute on it, otherwise pass through."
-        }
-
-        return ConfSchema(json=json)
-
-    def ports_setup(self):
-        port_type = PortsSpecSchema.port_type
-
-        input_connections = self.get_connected_inports()
-        if (self.INPUT_PORT_NAME in input_connections):
-            determined_type = input_connections[self.INPUT_PORT_NAME]
-            inports = {self.INPUT_PORT_NAME: {port_type: determined_type}}
-        else:
-            intypes = [cudf.DataFrame, dask_cudf.DataFrame,
-                       pd.DataFrame, DaskDataFrame]
-            inports = {self.INPUT_PORT_NAME: {port_type: intypes}}
-
-        out_types = [cudf.DataFrame, pd.DataFrame]
-        outports = {self.OUTPUT_PORT_NAME: {port_type: out_types}}
-
-        return NodePorts(inports=inports, outports=outports)
-
-    def meta_setup(self):
-        # no required columns
-        required = {}
-        '''Pass through columns from inputs to outputs'''
-        return _PortTypesMixin.meta_setup(self, required=required)
-
-    def process(self, inputs):
-        din = inputs[self.INPUT_PORT_NAME]
-        dout = din.compute() if isinstance(din, DaskDataFrame) else din
-
-        return {self.OUTPUT_PORT_NAME: dout}
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/datetimeFilterNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/datetimeFilterNode.py
index 885aa8c2..05a69ada 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/datetimeFilterNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/datetimeFilterNode.py
@@ -1,13 +1,14 @@
 import datetime
-from greenflow.dataframe_flow import Node
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow import Node, PortsSpecSchema
 from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
-
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 __all__ = ['DatetimeFilterNode']
 
 
-class DatetimeFilterNode(_PortTypesMixin, Node):
+class DatetimeFilterNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
     """
     A node that is used to select datapoints based on range of time.
     conf["beg"] defines the beginning of the date inclusively and
@@ -17,17 +18,43 @@ class DatetimeFilterNode(_PortTypesMixin, Node):
     """
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'stock_in'
         self.OUTPUT_PORT_NAME = 'stock_out'
-
-    def meta_setup(self):
-        cols_required = {"datetime": "date"}
-        return _PortTypesMixin.meta_setup(self,
-                                          required=cols_required)
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:stock_in}"
+            }
+        }
+        addition = {}
+        cols_required = {"datetime": "datetime64[ns]"}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: addition
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -78,11 +105,13 @@ def process(self, inputs):
         dataframe
         """
         df = inputs[self.INPUT_PORT_NAME]
-        beg_date = \
-            datetime.datetime.strptime(self.conf['beg'], '%Y-%m-%d')
-        end_date = \
-            datetime.datetime.strptime(self.conf['end'], '%Y-%m-%d')
+        beg_date = datetime.datetime.strptime(self.conf['beg'],
+                                              '%Y-%m-%dT%H:%M:%S.%fZ')
+        end_date = datetime.datetime.strptime(self.conf['end'],
+                                              '%Y-%m-%dT%H:%M:%S.%fZ')
         df = df.query('datetime<@end_date and datetime>=@beg_date',
-                      local_dict={'beg_date': beg_date,
-                                  'end_date': end_date})
+                      local_dict={
+                          'beg_date': beg_date,
+                          'end_date': end_date
+                      })
         return {self.OUTPUT_PORT_NAME: df}
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/dropNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/dropNode.py
index e08a6e07..fd85a8e0 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/dropNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/dropNode.py
@@ -1,27 +1,53 @@
-from greenflow.dataframe_flow import Node
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow import Node, PortsSpecSchema
 from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
+__all__ = ['DropNode']
 
-class DropNode(_PortTypesMixin, Node):
 
-    def init(self):
-        _PortTypesMixin.init(self)
-
-    def meta_setup(self):
-        cols_required = {}
-        if 'columns' in self.conf:
-            dropped = {}
-            for k in self.conf['columns']:
-                dropped[k] = None
-            return _PortTypesMixin.deletion_meta_setup(self,
-                                                       dropped,
-                                                       required=cols_required)
-        else:
-            return _PortTypesMixin.meta_setup(self, required=cols_required)
+class DropNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
+    def init(self):
+        TemplateNodeMixin.init(self)
+        port_type = PortsSpecSchema.port_type
+        self.INPUT_PORT_NAME = 'in'
+        self.OUTPUT_PORT_NAME = 'out'
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:in}"
+            }
+        }
+        meta_inports = {
+            self.INPUT_PORT_NAME: {}
+        }
+        dropped = {}
+        for k in self.conf.get('columns', {}):
+            dropped[k] = None
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_DELETION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: dropped
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/indicatorNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/indicatorNode.py
index c54d5688..3327be83 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/indicatorNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/indicatorNode.py
@@ -1,8 +1,14 @@
-from .. import cuindicator as ci
-from .._port_type_node import _PortTypesMixin
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
-from greenflow.dataframe_flow import Node
 import copy
+from greenflow.dataframe_flow import (ConfSchema, PortsSpecSchema)
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow import Node
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
+
+from .. import cuindicator as ci
+
+__all__ = ['IndicatorNode']
+
 
 IN_DATA = {
     "port_exponential_moving_average": {
@@ -144,15 +150,27 @@
 }
 
 
-class IndicatorNode(_PortTypesMixin, Node):
+class IndicatorNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.delayed_process = True
         self.INPUT_PORT_NAME = 'stock_in'
         self.OUTPUT_PORT_NAME = 'stock_out'
-
-    def meta_setup(self):
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:stock_in}"
+            }
+        }
         cols_required = {'indicator': 'int32'}
         addition = {}
         if 'indicators' in self.conf:
@@ -174,11 +192,24 @@ def meta_setup(self):
                 else:
                     out_col = self._compose_name(conf, [])
                     addition[out_col] = 'float64'
-        return _PortTypesMixin.addition_meta_setup(self, addition,
-                                                   required=cols_required)
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: addition
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def _compose_name(self, indicator, outname=[]):
         name = indicator['function']
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/leftMergeNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/leftMergeNode.py
index 402c62a1..af068032 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/leftMergeNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/leftMergeNode.py
@@ -1,97 +1,83 @@
 from greenflow.dataframe_flow import Node
 from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
-from greenflow.dataframe_flow.portsSpecSchema import (PortsSpecSchema,
-                                                   MetaData,
-                                                   NodePorts)
-import cudf
-import dask_cudf
-import pandas as pd
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow.portsSpecSchema import PortsSpecSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
+__all__ = ['LeftMergeNode']
 
-class LeftMergeNode(_PortTypesMixin, Node):
+
+class LeftMergeNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_LEFT_NAME = 'left'
         self.INPUT_PORT_RIGHT_NAME = 'right'
         self.OUTPUT_PORT_NAME = 'merged'
-
-    def ports_setup_from_types(self, types):
         port_type = PortsSpecSchema.port_type
-        input_ports = {
+        port_inports = {
             self.INPUT_PORT_LEFT_NAME: {
-                port_type: types
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
             },
             self.INPUT_PORT_RIGHT_NAME: {
-                port_type: types
-            }
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
         }
-
-        output_ports = {
+        port_outports = {
             self.OUTPUT_PORT_NAME: {
-                port_type: types
+                port_type: "${port:left}"
             }
         }
-
-        input_connections = self.get_connected_inports()
-        if (self.INPUT_PORT_LEFT_NAME in input_connections and
-                self.INPUT_PORT_RIGHT_NAME in input_connections):
-            determined_type1 = input_connections[self.INPUT_PORT_LEFT_NAME]
-            determined_type2 = input_connections[self.INPUT_PORT_RIGHT_NAME]
-            if (determined_type1 == determined_type2):
-                # connected
-                return NodePorts(inports={self.INPUT_PORT_LEFT_NAME: {
-                    port_type: determined_type1},
-                    self.INPUT_PORT_RIGHT_NAME: {
-                    port_type: determined_type1}},
-                    outports={self.OUTPUT_PORT_NAME: {
-                        port_type: determined_type1}})
-            else:
-                return NodePorts(inports=input_ports, outports=output_ports)
-        else:
-            return NodePorts(inports=input_ports, outports=output_ports)
-
-    def ports_setup(self):
-        types = [cudf.DataFrame,
-                 dask_cudf.DataFrame,
-                 pd.DataFrame]
-        return self.ports_setup_from_types(types)
-
-    def meta_setup(self):
         cols_required = {}
-        required = {
+        meta_inports = {
             self.INPUT_PORT_LEFT_NAME: cols_required,
             self.INPUT_PORT_RIGHT_NAME: cols_required
         }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: {}
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
+
+    def update(self):
+        TemplateNodeMixin.update(self)
         input_meta = self.get_input_meta()
+        output_cols = {}
         if (self.INPUT_PORT_LEFT_NAME in input_meta
                 and self.INPUT_PORT_RIGHT_NAME in input_meta):
             col_from_left_inport = input_meta[self.INPUT_PORT_LEFT_NAME]
             col_from_right_inport = input_meta[self.INPUT_PORT_RIGHT_NAME]
             col_from_left_inport.update(col_from_right_inport)
-            output_cols = {
-                self.OUTPUT_PORT_NAME: col_from_left_inport
-            }
-            metadata = MetaData(inports=required, outports=output_cols)
-            return metadata
+            output_cols = col_from_left_inport
         elif self.INPUT_PORT_LEFT_NAME in input_meta:
             col_from_left_inport = input_meta[self.INPUT_PORT_LEFT_NAME]
-            output_cols = {
-                self.OUTPUT_PORT_NAME: col_from_left_inport
-            }
-            metadata = MetaData(inports=required, outports=output_cols)
-            return metadata
+            output_cols = col_from_left_inport
         elif self.INPUT_PORT_RIGHT_NAME in input_meta:
             col_from_right_inport = input_meta[self.INPUT_PORT_RIGHT_NAME]
-            output_cols = {
-                self.OUTPUT_PORT_NAME: col_from_right_inport
-            }
-            metadata = MetaData(inports=required, outports=output_cols)
-            return metadata
-        else:
-            metadata = MetaData(inports=required,
-                                outports={self.OUTPUT_PORT_NAME: {}})
-            return metadata
+            output_cols = col_from_right_inport
+        meta_outports = self.template_meta_setup().outports
+        meta_outports[self.OUTPUT_PORT_NAME][MetaDataSchema.META_DATA] = \
+            output_cols
+        self.template_meta_setup(
+            in_ports=None,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/linearEmbedding.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/linearEmbedding.py
index f99d960a..47dcbef7 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/linearEmbedding.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/linearEmbedding.py
@@ -1,63 +1,57 @@
-from greenflow.dataframe_flow import Node
-from .._port_type_node import _PortTypesMixin
-from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema,
-                                                      PortsSpecSchema,
-                                                      NodePorts, MetaData)
-from .data_obj import ProjectionData
 import cupy as cp
 import copy
 from collections import OrderedDict
 
+from greenflow.dataframe_flow import Node
+from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema,
+                                                      PortsSpecSchema)
+from greenflow.dataframe_flow.metaSpec import MetaData
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
+
+from .data_obj import ProjectionData
+
 __all__ = ['LinearEmbeddingNode']
 
 SPECIAL_OUTPUT_DIM_COL = 'OUTPUT_DIM_23b1c5ce-e0bf-11ea-afcf-80e82cc76d44'
 
 
-class LinearEmbeddingNode(_PortTypesMixin, Node):
+class LinearEmbeddingNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'df_in'
         self.OUTPUT_PORT_NAME = 'df_out'
         self.INPUT_PROJ_NAME = 'proj_data_in'
         self.OUTPUT_PROJ_NAME = 'proj_data_out'
-
-    def ports_setup_from_types(self, types):
-        """
-        overwrite the _PortTypesMixin.ports_setup_from_types
-        method, which is invoked by the _PortTypesMixin.ports_setup
-        """
         port_type = PortsSpecSchema.port_type
-        input_ports = {
+        port_inports = {
             self.INPUT_PORT_NAME: {
-                port_type: types
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
             },
             self.INPUT_PROJ_NAME: {
-                port_type: ProjectionData
+                port_type: [
+                    "greenflow_gquant_plugin.transform.data_obj.ProjectionData"
+                ]
             }
         }
-
-        output_ports = {
+        port_outports = {
             self.OUTPUT_PORT_NAME: {
-                port_type: types
+                port_type: "${port:df_in}"
             },
             self.OUTPUT_PROJ_NAME: {
-                port_type: ProjectionData
-            }
+                port_type: [
+                    "greenflow_gquant_plugin.transform.data_obj.ProjectionData"
+                ]
+            },
         }
-
-        input_connections = self.get_connected_inports()
-        if self.INPUT_PORT_NAME in input_connections:
-            determined_type = input_connections[self.INPUT_PORT_NAME]
-            input_ports.update({self.INPUT_PORT_NAME:
-                                {port_type: determined_type}})
-            output_ports.update({self.OUTPUT_PORT_NAME: {
-                                 port_type: determined_type}})
-            # connected
-            return NodePorts(inports=input_ports,
-                             outports=output_ports)
-        else:
-            return NodePorts(inports=input_ports, outports=output_ports)
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
 
     def meta_setup(self):
         required = {
@@ -153,9 +147,6 @@ def meta_setup(self):
         metadata = MetaData(inports=required, outports=output_cols)
         return metadata
 
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
-
     def conf_schema(self):
         json = {
             "title": "Linear Embeding configure",
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/maxNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/maxNode.py
index aec628e5..e342a9c6 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/maxNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/maxNode.py
@@ -1,17 +1,54 @@
-from greenflow.dataframe_flow import Node
-from .._port_type_node import _PortTypesMixin
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow import (
+    Node, ConfSchema, PortsSpecSchema, MetaDataSchema)
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
+__all__ = ['MaxNode']
 
-class MaxNode(_PortTypesMixin, Node):
+
+class MaxNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'in'
         self.OUTPUT_PORT_NAME = 'out'
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:in}"
+            }
+        }
+        cols_required = {"asset": "int64"}
+        if 'column' in self.conf:
+            retention = {self.conf['column']: "float64",
+                         "asset": "int64"}
+        else:
+            retention = {"asset": "int64"}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: retention
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -58,17 +95,3 @@ def process(self, inputs):
                               "asset"]].groupby(["asset"]).max().reset_index()
         volume_df.columns = ['asset', max_column]
         return {self.OUTPUT_PORT_NAME: volume_df}
-
-    def meta_setup(self):
-        cols_required = {"asset": "int64"}
-        if 'column' in self.conf:
-            retention = {self.conf['column']: "float64",
-                         "asset": "int64"}
-            return _PortTypesMixin.retention_meta_setup(self,
-                                                        retention,
-                                                        required=cols_required)
-        else:
-            retention = {"asset": "int64"}
-            return _PortTypesMixin.retention_meta_setup(self,
-                                                        retention,
-                                                        required=cols_required)
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/minNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/minNode.py
index 42cda39e..601ff466 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/minNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/minNode.py
@@ -1,17 +1,55 @@
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow import (
+    ConfSchema, PortsSpecSchema, MetaDataSchema)
 from greenflow.dataframe_flow import Node
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
+__all__ = ['MinNode']
 
-class MinNode(_PortTypesMixin, Node):
+
+class MinNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'in'
         self.OUTPUT_PORT_NAME = 'out'
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:in}"
+            }
+        }
+        cols_required = {"asset": "int64"}
+        if 'column' in self.conf:
+            retention = {self.conf['column']: "float64",
+                         "asset": "int64"}
+        else:
+            retention = {"asset": "int64"}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: retention
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -59,17 +97,3 @@ def process(self, inputs):
                               "asset"]].groupby(["asset"]).min().reset_index()
         volume_df.columns = ['asset', min_column]
         return {self.OUTPUT_PORT_NAME: volume_df}
-
-    def meta_setup(self):
-        cols_required = {"asset": "int64"}
-        if 'column' in self.conf:
-            retention = {self.conf['column']: "float64",
-                         "asset": "int64"}
-            return _PortTypesMixin.retention_meta_setup(self,
-                                                        retention,
-                                                        required=cols_required)
-        else:
-            retention = {"asset": "int64"}
-            return _PortTypesMixin.retention_meta_setup(self,
-                                                        retention,
-                                                        required=cols_required)
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/normalizationNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/normalizationNode.py
index 4ff99437..c3969093 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/normalizationNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/normalizationNode.py
@@ -1,56 +1,51 @@
+from collections import OrderedDict
 from greenflow.dataframe_flow import Node
-from .._port_type_node import _PortTypesMixin
-from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema, MetaData,
-                                                   PortsSpecSchema, NodePorts)
+from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema,
+                                                      PortsSpecSchema)
+from greenflow.dataframe_flow.metaSpec import MetaData
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 from .data_obj import NormalizationData
-from collections import OrderedDict
+
+__all__ = ['NormalizationNode']
 
 
-class NormalizationNode(_PortTypesMixin, Node):
+class NormalizationNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'df_in'
         self.OUTPUT_PORT_NAME = 'df_out'
         self.INPUT_NORM_MODEL_NAME = 'norm_data_in'
         self.OUTPUT_NORM_MODEL_NAME = 'norm_data_out'
-
-    def ports_setup_from_types(self, types):
-        """
-        overwrite the _PortTypesMixin.ports_setup_from_types
-        method, which is invoked by the _PortTypesMixin.ports_setup
-        """
         port_type = PortsSpecSchema.port_type
-        input_ports = {
+        port_inports = {
             self.INPUT_PORT_NAME: {
-                port_type: types
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
             },
             self.INPUT_NORM_MODEL_NAME: {
-                port_type: NormalizationData
+                port_type: [
+                    "greenflow_gquant_plugin.transform.data_obj.NormalizationData"  # noqa
+                ]
             }
         }
-
-        output_ports = {
+        port_outports = {
             self.OUTPUT_PORT_NAME: {
-                port_type: types
+                port_type: "${port:df_in}"
             },
             self.OUTPUT_NORM_MODEL_NAME: {
-                port_type: NormalizationData
-            }
+                port_type: [
+                    "greenflow_gquant_plugin.transform.data_obj.NormalizationData"  # noqa
+                ]
+            },
         }
-
-        input_connections = self.get_connected_inports()
-        if self.INPUT_PORT_NAME in input_connections:
-            determined_type = input_connections[self.INPUT_PORT_NAME]
-            input_ports.update({self.INPUT_PORT_NAME:
-                                {port_type: determined_type}})
-            output_ports.update({self.OUTPUT_PORT_NAME: {
-                                 port_type: determined_type}})
-            # connected
-            return NodePorts(inports=input_ports,
-                             outports=output_ports)
-        else:
-            return NodePorts(inports=input_ports, outports=output_ports)
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
 
     def meta_setup(self):
         cols_required = {}
@@ -128,9 +123,6 @@ def meta_setup(self):
             metadata.inports.pop(self.INPUT_NORM_MODEL_NAME, None)
         return metadata
 
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
-
     def conf_schema(self):
         json = {
             "title": "Normalization Node configure",
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/onehotEncoding.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/onehotEncoding.py
index 73895f2a..2ce2f2e3 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/onehotEncoding.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/onehotEncoding.py
@@ -1,20 +1,57 @@
-from greenflow.dataframe_flow import Node
+from greenflow.dataframe_flow import Node, PortsSpecSchema
 from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 __all__ = ["OneHotEncodingNode"]
 
 
-class OneHotEncodingNode(_PortTypesMixin, Node):
+class OneHotEncodingNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'in'
         self.OUTPUT_PORT_NAME = 'out'
         self.delayed_process = True
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:in}"
+            }
+        }
+        cols_required = {}
+        addition = {}
+        for col in self.conf:
+            for cat in col['cats']:
+                name = col.get('prefix')+col.get('prefix_sep', '_')+str(cat)
+                addition.update({name: col.get('dtype', 'float64')})
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: addition
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -82,14 +119,3 @@ def process(self, inputs):
         for col in self.conf:
             input_df = input_df.one_hot_encoding(**col)
         return {self.OUTPUT_PORT_NAME: input_df}
-
-    def meta_setup(self):
-        cols_required = {}
-        addition = {}
-        for col in self.conf:
-            for cat in col['cats']:
-                name = col.get('prefix')+col.get('prefix_sep', '_')+str(cat)
-                addition.update({name: col.get('dtype', 'float64')})
-        return _PortTypesMixin.addition_meta_setup(self,
-                                                   addition,
-                                                   required=cols_required)
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/persistNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/persistNode.py
deleted file mode 100644
index c5fdc04b..00000000
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/persistNode.py
+++ /dev/null
@@ -1,71 +0,0 @@
-from greenflow.dataframe_flow import (ConfSchema, PortsSpecSchema, NodePorts,
-                                      MetaData)
-from greenflow_gquant_plugin._port_type_node import _PortTypesMixin
-from greenflow.dataframe_flow import Node
-from dask.dataframe import DataFrame as DaskDataFrame
-from dask.distributed import wait
-import dask.distributed
-
-
-class PersistNode(_PortTypesMixin, Node):
-
-    def init(self):
-        _PortTypesMixin.init(self)
-
-    def ports_setup(self):
-        dy = PortsSpecSchema.dynamic
-        port_type = PortsSpecSchema.port_type
-        o_inports = {}
-        o_outports = {}
-        o_inports[self.INPUT_PORT_NAME] = {
-            port_type: [DaskDataFrame],
-            dy: True
-        }
-        input_connections = self.get_connected_inports()
-        for port_name in input_connections.keys():
-            if port_name != self.INPUT_PORT_NAME:
-                determined_type = input_connections[port_name]
-                o_outports[port_name] = {port_type: determined_type}
-        return NodePorts(inports=o_inports, outports=o_outports)
-
-    def conf_schema(self):
-        json = {
-            "title": "Persist the dask dataframe",
-            "type": "object",
-            "properties": {
-            },
-        }
-
-        ui = {
-        }
-        return ConfSchema(json=json, ui=ui)
-
-    def meta_setup(self):
-        input_meta = self.get_input_meta()
-        input_connections = self.get_connected_inports()
-        input_cols = {}
-        output_cols = {}
-        for port_name in input_connections.keys():
-            if port_name in input_meta:
-                meta = input_meta[port_name]
-                input_cols[port_name] = meta
-                output_cols[port_name] = meta
-        meta_data = MetaData(inports=input_cols, outports=output_cols)
-        return meta_data
-
-    def process(self, inputs):
-        # df = df.drop('datetime', axis=1)
-        output = {}
-        client = dask.distributed.client.default_client()
-        input_connections = self.get_connected_inports()
-        objs = []
-        for port_name in input_connections.keys():
-            if port_name != self.INPUT_PORT_NAME:
-                df = inputs[port_name]
-                objs.append(df)
-        objs = client.persist(objs)
-        wait([objs])
-        for port_name in input_connections.keys():
-            if port_name != self.INPUT_PORT_NAME:
-                output[port_name] = objs.pop(0)
-        return output
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/renameNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/renameNode.py
index 740f02bb..c892dc47 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/renameNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/renameNode.py
@@ -1,17 +1,69 @@
 from greenflow.dataframe_flow import Node
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow.portsSpecSchema import (PortsSpecSchema,
+                                                      ConfSchema)
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
+__all__ = ['RenameNode']
 
-class RenameNode(_PortTypesMixin, Node):
+
+class RenameNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'in'
         self.OUTPUT_PORT_NAME = 'out'
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:in}"
+            }
+        }
+        cols_required = {}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: {}
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
+    def update(self):
+        TemplateNodeMixin.update(self)
+        retention = {}
+        if 'new' in self.conf and 'old' in self.conf:
+            input_meta = self.get_input_meta()
+            if self.INPUT_PORT_NAME not in input_meta:
+                retention = {}
+            else:
+                col_from_inport = input_meta[self.INPUT_PORT_NAME]
+                oldType = col_from_inport[self.conf['old']]
+                del col_from_inport[self.conf['old']]
+                col_from_inport[self.conf['new']] = oldType
+                retention = col_from_inport
+        meta_outports = self.template_meta_setup().outports
+        meta_outports[self.OUTPUT_PORT_NAME][MetaDataSchema.META_DATA] = \
+            retention
+        self.template_meta_setup(in_ports=None, out_ports=meta_outports)
 
     def conf_schema(self):
         json = {
@@ -61,24 +113,3 @@ def process(self, inputs):
         old_column = self.conf['old']
         return {self.OUTPUT_PORT_NAME: input_df.rename(columns={
             old_column: new_column})}
-
-    def meta_setup(self):
-        empty = {}
-        if 'new' in self.conf and 'old' in self.conf:
-            input_meta = self.get_input_meta()
-            if self.INPUT_PORT_NAME not in input_meta:
-                return _PortTypesMixin.retention_meta_setup(self,
-                                                            {},
-                                                            required=empty)
-            else:
-                col_from_inport = input_meta[self.INPUT_PORT_NAME]
-                oldType = col_from_inport[self.conf['old']]
-                del col_from_inport[self.conf['old']]
-                col_from_inport[self.conf['new']] = oldType
-                return _PortTypesMixin.retention_meta_setup(self,
-                                                            col_from_inport,
-                                                            required=empty)
-        else:
-            return _PortTypesMixin.retention_meta_setup(self,
-                                                        {},
-                                                        required=empty)
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/returnFeatureNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/returnFeatureNode.py
index 2c9261b0..989b1940 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/returnFeatureNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/returnFeatureNode.py
@@ -1,23 +1,54 @@
-from greenflow.dataframe_flow import Node
-from .._port_type_node import _PortTypesMixin
+from greenflow.dataframe_flow import Node, PortsSpecSchema
 from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
+__all__ = ['ReturnFeatureNode']
 
-class ReturnFeatureNode(_PortTypesMixin, Node):
+
+class ReturnFeatureNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
         self.delayed_process = True
-        _PortTypesMixin.init(self)
         self.INPUT_PORT_NAME = 'stock_in'
         self.OUTPUT_PORT_NAME = 'stock_out'
-
-    def meta_setup(self):
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:stock_in}"
+            }
+        }
         cols_required = {"close": "float64",
                          "asset": "int64"}
         addition = {"returns": "float64"}
-        return _PortTypesMixin.addition_meta_setup(self,
-                                                   addition,
-                                                   required=cols_required)
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: addition
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -31,9 +62,6 @@ def conf_schema(self):
         }
         return ConfSchema(json=json, ui=ui)
 
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
-
     def process(self, inputs):
         """
         Add the rate of of return column based on the `close` price for each
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/sortNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/sortNode.py
index d6bfc681..51da6c47 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/sortNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/sortNode.py
@@ -1,21 +1,54 @@
-from .._port_type_node import _PortTypesMixin
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.portsSpecSchema import (PortsSpecSchema,
+                                                      ConfSchema)
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
 from greenflow.dataframe_flow import Node
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
+__all__ = ['SortNode']
 
-class SortNode(_PortTypesMixin, Node):
+
+class SortNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
+        TemplateNodeMixin.init(self)
+        self.INPUT_PORT_NAME = 'in'
+        self.OUTPUT_PORT_NAME = 'out'
         self.delayed_process = True
-        _PortTypesMixin.init(self)
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
-
-    def meta_setup(self):
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:in}"
+            }
+        }
         cols_required = {}
-        return _PortTypesMixin.meta_setup(self,
-                                          required=cols_required)
+        addition = {}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: addition
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/valueFilterNode.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/valueFilterNode.py
index 015a3bd2..da257121 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/valueFilterNode.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/transform/valueFilterNode.py
@@ -1,19 +1,53 @@
 from greenflow.dataframe_flow import Node
-from .._port_type_node import _PortTypesMixin
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema,
+                                                      PortsSpecSchema)
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
+__all__ = ['ValueFilterNode']
 
-class ValueFilterNode(_PortTypesMixin, Node):
 
-    def init(self):
-        _PortTypesMixin.init(self)
+class ValueFilterNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
-    def meta_setup(self):
+    def init(self):
+        TemplateNodeMixin.init(self)
+        self.INPUT_PORT_NAME = 'in'
+        self.OUTPUT_PORT_NAME = 'out'
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:in}"
+            }
+        }
         cols_required = {"asset": "int64"}
-        return _PortTypesMixin.meta_setup(self, required=cols_required)
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
+        addition = {}
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: addition
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
diff --git a/gQuant/plugins/gquant_plugin/notebooks/01_tutorial.ipynb b/gQuant/plugins/gquant_plugin/notebooks/01_tutorial.ipynb
index 90feb9a0..f3d5aae6 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/01_tutorial.ipynb
+++ b/gQuant/plugins/gquant_plugin/notebooks/01_tutorial.ipynb
@@ -67,7 +67,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "aa99803de134434495790ac9be5c9871",
+       "model_id": "5ec73b8f328745fd8403d1821acd4ffb",
        "version_major": 2,
        "version_minor": 0
       },
@@ -132,12 +132,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "fb40274ceb44404f8221b1af3f9b94e5",
+       "model_id": "85b5f62ea12e4a8fadb0a9f4231075a9",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -164,12 +164,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "24c9bc5ac191466a978d6cde8d668b9c",
+       "model_id": "59ce74a2129648c984af82ce1e736a38",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -351,18 +351,18 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 27,
+   "execution_count": 6,
    "metadata": {},
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "9fe6df6a63b0413bb8ec6f1f4c159bcc",
+       "model_id": "84462c2c25e64a4bb8a1b271adf40773",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -394,7 +394,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 28,
+   "execution_count": 7,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -412,7 +412,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 8,
    "metadata": {},
    "outputs": [
     {
@@ -429,7 +429,7 @@
       "  type: ValueFilterNode\n",
       "  conf:\n",
       "  - column: volume\n",
-      "    min: 50\n",
+      "    min: 50.0\n",
       "  inputs:\n",
       "    in: stock_data.cudf_out\n",
       "  module: greenflow_gquant_plugin.transform\n",
@@ -455,18 +455,18 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 9,
    "metadata": {},
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "3c5f7fabeb3046f5a459c8f769e6e5f7",
+       "model_id": "e9fecec9e8a74dcd906682bd96cecea1",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -495,7 +495,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 10,
    "metadata": {},
    "outputs": [
     {
@@ -504,17 +504,17 @@
      "text": [
       "Output of build task graph are instances of each task in a dictionary:\n",
       "\n",
-      "stock_data: <NodeInTaskGraph greenflow_gquant_plugin.dataloader.csvStockLoader.CsvStockLoader object at 0x7ff4ecb7cbe0>\n",
-      "volume_filter: <NodeInTaskGraph greenflow_gquant_plugin.transform.valueFilterNode.ValueFilterNode object at 0x7ff4ecb7cd30>\n",
-      "sort_node: <NodeInTaskGraph greenflow_gquant_plugin.transform.sortNode.SortNode object at 0x7ff4ecb7cf40>\n",
-      "add_return_feature: <NodeInTaskGraph greenflow_gquant_plugin.transform.returnFeatureNode.ReturnFeatureNode object at 0x7ff4ecb7c520>\n",
-      "stock_name: <NodeInTaskGraph greenflow_gquant_plugin.dataloader.stockNameLoader.StockNameLoader object at 0x7ff4ecb7c310>\n",
-      "average_volume: <NodeInTaskGraph greenflow_gquant_plugin.transform.averageNode.AverageNode object at 0x7ff4ecb7c100>\n",
-      "average_return: <NodeInTaskGraph greenflow_gquant_plugin.transform.averageNode.AverageNode object at 0x7ff4ecdc1580>\n",
-      "left_merge1: <NodeInTaskGraph greenflow_gquant_plugin.transform.leftMergeNode.LeftMergeNode object at 0x7ff4ecdc1f40>\n",
-      "left_merge2: <NodeInTaskGraph greenflow_gquant_plugin.transform.leftMergeNode.LeftMergeNode object at 0x7ff4ecdc17c0>\n",
-      "output_csv1: <NodeInTaskGraph greenflow_gquant_plugin.analysis.outCsvNode.OutCsvNode object at 0x7ff4ecdc1520>\n",
-      "output_csv2: <NodeInTaskGraph greenflow_gquant_plugin.analysis.outCsvNode.OutCsvNode object at 0x7ff4ecdc1850>\n",
+      "stock_data: <NodeInTaskGraph greenflow_gquant_plugin.dataloader.csvStockLoader.CsvStockLoader object at 0x7faf857b7d00>\n",
+      "volume_filter: <NodeInTaskGraph greenflow_gquant_plugin.transform.valueFilterNode.ValueFilterNode object at 0x7faf857b7d30>\n",
+      "sort_node: <NodeInTaskGraph greenflow_gquant_plugin.transform.sortNode.SortNode object at 0x7faf8575d6a0>\n",
+      "add_return_feature: <NodeInTaskGraph greenflow_gquant_plugin.transform.returnFeatureNode.ReturnFeatureNode object at 0x7faf8575d730>\n",
+      "stock_name: <NodeInTaskGraph greenflow_gquant_plugin.dataloader.stockNameLoader.StockNameLoader object at 0x7faf8575d9d0>\n",
+      "average_volume: <NodeInTaskGraph greenflow_gquant_plugin.transform.averageNode.AverageNode object at 0x7faf857b7ee0>\n",
+      "average_return: <NodeInTaskGraph greenflow_gquant_plugin.transform.averageNode.AverageNode object at 0x7fb0d953c730>\n",
+      "left_merge1: <NodeInTaskGraph greenflow_gquant_plugin.transform.leftMergeNode.LeftMergeNode object at 0x7faf857780d0>\n",
+      "left_merge2: <NodeInTaskGraph greenflow_gquant_plugin.transform.leftMergeNode.LeftMergeNode object at 0x7faf857b7100>\n",
+      "output_csv1: <NodeInTaskGraph greenflow_gquant_plugin.analysis.outCsvNode.OutCsvNode object at 0x7faf8575dd30>\n",
+      "output_csv2: <NodeInTaskGraph greenflow_gquant_plugin.analysis.outCsvNode.OutCsvNode object at 0x7faf8575d880>\n",
       "\n"
      ]
     }
@@ -530,7 +530,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 11,
    "metadata": {},
    "outputs": [
     {
@@ -539,7 +539,7 @@
      "text": [
       "output meta in outgoing dataframe:\n",
       "\n",
-      "MetaData(inports={'left': {}, 'right': {}}, outports={'merged': {'returns': 'float64', 'asset': 'int64', 'asset_name': 'object'}})\n"
+      "MetaData(inports={'left': {}, 'right': {}}, outports={'merged': {'asset': 'int64', 'returns': 'float64', 'asset_name': 'object'}})\n"
      ]
     }
    ],
@@ -563,20 +563,13 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 13,
+   "execution_count": 12,
    "metadata": {},
    "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "\n"
-     ]
-    },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "c64c1e5f75d84692a2b085a5d66fc286",
+       "model_id": "436fb819fbc94830af64aafe0fb6a658",
        "version_major": 2,
        "version_minor": 0
       },
@@ -602,7 +595,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 14,
+   "execution_count": 13,
    "metadata": {},
    "outputs": [
     {
@@ -634,33 +627,33 @@
        "  <tbody>\n",
        "    <tr>\n",
        "      <th>0</th>\n",
-       "      <td>869577</td>\n",
-       "      <td>154.528596</td>\n",
-       "      <td>CTT</td>\n",
+       "      <td>24568</td>\n",
+       "      <td>203.002419</td>\n",
+       "      <td>ORN</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>1</th>\n",
-       "      <td>869584</td>\n",
-       "      <td>701.630560</td>\n",
-       "      <td>LPT</td>\n",
+       "      <td>2557</td>\n",
+       "      <td>429.953169</td>\n",
+       "      <td>EMMS</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>2</th>\n",
-       "      <td>869587</td>\n",
-       "      <td>119.878161</td>\n",
-       "      <td>HBP</td>\n",
+       "      <td>4142</td>\n",
+       "      <td>487.567188</td>\n",
+       "      <td>RIGL</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>3</th>\n",
-       "      <td>869589</td>\n",
-       "      <td>161.938559</td>\n",
-       "      <td>DSLV</td>\n",
+       "      <td>869369</td>\n",
+       "      <td>172.961884</td>\n",
+       "      <td>IBP</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>4</th>\n",
-       "      <td>869590</td>\n",
-       "      <td>204.126667</td>\n",
-       "      <td>BPTH</td>\n",
+       "      <td>705684</td>\n",
+       "      <td>107.933333</td>\n",
+       "      <td>USMD</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>...</th>\n",
@@ -670,33 +663,33 @@
        "    </tr>\n",
        "    <tr>\n",
        "      <th>4995</th>\n",
-       "      <td>7438</td>\n",
-       "      <td>80.495558</td>\n",
-       "      <td>NQM</td>\n",
+       "      <td>4725</td>\n",
+       "      <td>1473.216105</td>\n",
+       "      <td>UTSI</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>4996</th>\n",
-       "      <td>7440</td>\n",
-       "      <td>76.090120</td>\n",
-       "      <td>NQP</td>\n",
+       "      <td>4136</td>\n",
+       "      <td>81.733333</td>\n",
+       "      <td>RGCO</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>4997</th>\n",
-       "      <td>7441</td>\n",
-       "      <td>86.637237</td>\n",
-       "      <td>NQS</td>\n",
+       "      <td>795091</td>\n",
+       "      <td>73.178571</td>\n",
+       "      <td>FRBA</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>4998</th>\n",
-       "      <td>7443</td>\n",
-       "      <td>569.610747</td>\n",
-       "      <td>NR</td>\n",
+       "      <td>3811</td>\n",
+       "      <td>230.635886</td>\n",
+       "      <td>ORIT</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>4999</th>\n",
-       "      <td>7455</td>\n",
-       "      <td>68.407921</td>\n",
-       "      <td>NRT</td>\n",
+       "      <td>4672</td>\n",
+       "      <td>131.653184</td>\n",
+       "      <td>UBNK</td>\n",
        "    </tr>\n",
        "  </tbody>\n",
        "</table>\n",
@@ -704,23 +697,23 @@
        "</div>"
       ],
       "text/plain": [
-       "       asset      volume asset_name\n",
-       "0     869577  154.528596        CTT\n",
-       "1     869584  701.630560        LPT\n",
-       "2     869587  119.878161        HBP\n",
-       "3     869589  161.938559       DSLV\n",
-       "4     869590  204.126667       BPTH\n",
-       "...      ...         ...        ...\n",
-       "4995    7438   80.495558        NQM\n",
-       "4996    7440   76.090120        NQP\n",
-       "4997    7441   86.637237        NQS\n",
-       "4998    7443  569.610747         NR\n",
-       "4999    7455   68.407921        NRT\n",
+       "       asset       volume asset_name\n",
+       "0      24568   203.002419        ORN\n",
+       "1       2557   429.953169       EMMS\n",
+       "2       4142   487.567188       RIGL\n",
+       "3     869369   172.961884        IBP\n",
+       "4     705684   107.933333       USMD\n",
+       "...      ...          ...        ...\n",
+       "4995    4725  1473.216105       UTSI\n",
+       "4996    4136    81.733333       RGCO\n",
+       "4997  795091    73.178571       FRBA\n",
+       "4998    3811   230.635886       ORIT\n",
+       "4999    4672   131.653184       UBNK\n",
        "\n",
        "[5000 rows x 3 columns]"
       ]
      },
-     "execution_count": 14,
+     "execution_count": 13,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -740,24 +733,24 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 15,
+   "execution_count": 14,
    "metadata": {},
    "outputs": [
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "id:stock_data process time:4.007s\n",
-      "id:volume_filter process time:0.009s\n",
-      "id:sort_node process time:0.087s\n",
-      "id:add_return_feature process time:0.054s\n",
-      "id:average_volume process time:0.015s\n",
+      "id:stock_data process time:3.168s\n",
+      "id:volume_filter process time:0.021s\n",
+      "id:sort_node process time:0.102s\n",
+      "id:add_return_feature process time:0.058s\n",
+      "id:average_volume process time:0.013s\n",
       "id:average_return process time:0.014s\n",
-      "id:stock_name process time:0.014s\n",
-      "id:left_merge1 process time:0.003s\n",
+      "id:stock_name process time:0.008s\n",
+      "id:left_merge1 process time:0.002s\n",
       "id:output_csv1 process time:0.015s\n",
       "id:left_merge2 process time:0.002s\n",
-      "id:output_csv2 process time:0.013s\n"
+      "id:output_csv2 process time:0.014s\n"
      ]
     }
    ],
@@ -775,7 +768,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 16,
+   "execution_count": 15,
    "metadata": {},
    "outputs": [
     {
@@ -784,33 +777,33 @@
      "text": [
       "csv_1_df content:\n",
       "       asset   returns asset_name\n",
-      "0     869577 -0.000295        CTT\n",
-      "1     869584  0.000387        LPT\n",
-      "2     869587  0.027713        HBP\n",
-      "3     869589  0.001337       DSLV\n",
-      "4     869590  0.009657       BPTH\n",
+      "0     869301 -0.005854      VNRBP\n",
+      "1       3159  0.000315       ISBC\n",
+      "2       8044  0.000516        SGU\n",
+      "3       2123  0.000801       CGNX\n",
+      "4      22873 -0.001068       RENN\n",
       "...      ...       ...        ...\n",
-      "4995   24076 -0.000566        STV\n",
-      "4996   24077  0.000285       INVN\n",
-      "4997   24078  0.045625        TCO\n",
-      "4998   24079  0.073684      WTFCW\n",
-      "4999   24088  0.001275        MCF\n",
+      "4995    3518  0.001136       MPWR\n",
+      "4996  707774 -0.000417       MODN\n",
+      "4997    4856  0.000979       WIRE\n",
+      "4998   22461 -0.000243         MY\n",
+      "4999    1973 -0.002916       BOCH\n",
       "\n",
       "[5000 rows x 3 columns]\n",
       "\n",
       "csv_2_df content:\n",
       "       asset       volume asset_name\n",
-      "0     869577   154.528596        CTT\n",
-      "1     869584   701.630560        LPT\n",
-      "2     869587   119.878161        HBP\n",
-      "3     869589   161.938559       DSLV\n",
-      "4     869590   204.126667       BPTH\n",
+      "0      24568   203.002419        ORN\n",
+      "1       2557   429.953169       EMMS\n",
+      "2       4142   487.567188       RIGL\n",
+      "3     869369   172.961884        IBP\n",
+      "4     705684   107.933333       USMD\n",
       "...      ...          ...        ...\n",
-      "4995   24076   328.960250        STV\n",
-      "4996   24077  1998.246290       INVN\n",
-      "4997   24078   443.700394        TCO\n",
-      "4998   24079   221.725000      WTFCW\n",
-      "4999   24088   128.905316        MCF\n",
+      "4995  869374   279.946042       WATT\n",
+      "4996  701990   302.973772       FRGI\n",
+      "4997   24636   136.807107       SVVC\n",
+      "4998    6190  2069.864690        FNF\n",
+      "4999   24153   887.397596        DAR\n",
       "\n",
       "[5000 rows x 3 columns]\n"
      ]
@@ -835,7 +828,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 17,
+   "execution_count": 16,
    "metadata": {},
    "outputs": [
     {
@@ -869,25 +862,25 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 18,
+   "execution_count": 17,
    "metadata": {},
    "outputs": [
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "       asset      volume\n",
-      "0         93   86.594737\n",
-      "1        165   67.020000\n",
-      "2        239  128.835141\n",
-      "3        281   68.450000\n",
-      "4        592  110.333333\n",
-      "...      ...         ...\n",
-      "4995  869589  161.938559\n",
-      "4996  869590  204.126667\n",
-      "4997  869592  101.423675\n",
-      "4998  869597   81.298788\n",
-      "4999  869599   83.425988\n",
+      "       asset       volume\n",
+      "0      22705    67.929114\n",
+      "1     869315   151.844770\n",
+      "2       2526    88.337888\n",
+      "3       3939    91.674194\n",
+      "4     705893  8616.574853\n",
+      "...      ...          ...\n",
+      "4995  869571   639.127042\n",
+      "4996    7842   709.077851\n",
+      "4997  701570   110.977778\n",
+      "4998  701705   970.310847\n",
+      "4999    4859   143.615344\n",
       "\n",
       "[5000 rows x 2 columns]\n"
      ]
@@ -934,7 +927,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 19,
+   "execution_count": 18,
    "metadata": {},
    "outputs": [
     {
@@ -944,17 +937,17 @@
       "Return mean Dataframe:\n",
       "\n",
       "       asset   returns\n",
-      "0         93  0.241380\n",
-      "1        165  0.000700\n",
-      "2        239  0.010021\n",
-      "3        281 -0.088465\n",
-      "4        592  0.619716\n",
+      "0      22705  0.001691\n",
+      "1     869315  0.000701\n",
+      "2       2526  0.002374\n",
+      "3       3939  0.052447\n",
+      "4     705893  0.000790\n",
       "...      ...       ...\n",
-      "4995  869589  0.001337\n",
-      "4996  869590  0.009657\n",
-      "4997  869592  0.001202\n",
-      "4998  869597 -0.003332\n",
-      "4999  869599  0.003291\n",
+      "4995  869571 -0.002908\n",
+      "4996    7842  0.000698\n",
+      "4997  701570 -0.004115\n",
+      "4998  701705  0.002157\n",
+      "4999    4859  0.008666\n",
       "\n",
       "[5000 rows x 2 columns]\n"
      ]
@@ -987,7 +980,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 20,
+   "execution_count": 19,
    "metadata": {},
    "outputs": [
     {
@@ -995,8 +988,8 @@
      "output_type": "stream",
      "text": [
       "Using in-memory dataframes for load:\n",
-      "CPU times: user 183 ms, sys: 88.8 ms, total: 272 ms\n",
-      "Wall time: 281 ms\n"
+      "CPU times: user 156 ms, sys: 76.8 ms, total: 233 ms\n",
+      "Wall time: 263 ms\n"
      ]
     }
    ],
@@ -1016,7 +1009,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 21,
+   "execution_count": 20,
    "metadata": {},
    "outputs": [
     {
@@ -1024,8 +1017,8 @@
      "output_type": "stream",
      "text": [
       "Using cached dataframes on disk for load:\n",
-      "CPU times: user 2.97 s, sys: 932 ms, total: 3.9 s\n",
-      "Wall time: 3.88 s\n"
+      "CPU times: user 2.52 s, sys: 488 ms, total: 3.01 s\n",
+      "Wall time: 3.02 s\n"
      ]
     }
    ],
@@ -1041,7 +1034,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 22,
+   "execution_count": 21,
    "metadata": {},
    "outputs": [
     {
@@ -1049,8 +1042,8 @@
      "output_type": "stream",
      "text": [
       "Re-running dataframes calculations instead of using load:\n",
-      "CPU times: user 2.94 s, sys: 989 ms, total: 3.93 s\n",
-      "Wall time: 3.89 s\n"
+      "CPU times: user 2.49 s, sys: 556 ms, total: 3.04 s\n",
+      "Wall time: 3.05 s\n"
      ]
     }
    ],
@@ -1072,15 +1065,15 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 23,
+   "execution_count": 22,
    "metadata": {},
    "outputs": [
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "CPU times: user 3.04 s, sys: 892 ms, total: 3.93 s\n",
-      "Wall time: 3.92 s\n"
+      "CPU times: user 2.52 s, sys: 459 ms, total: 2.98 s\n",
+      "Wall time: 3.01 s\n"
      ]
     }
    ],
@@ -1110,7 +1103,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 24,
+   "execution_count": 23,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -1154,24 +1147,45 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 26,
+   "execution_count": 24,
    "metadata": {},
    "outputs": [
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "class ValueFilterNode(_PortTypesMixin, Node):\n",
+      "class ValueFilterNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):\n",
       "\n",
       "    def init(self):\n",
-      "        _PortTypesMixin.init(self)\n",
-      "\n",
-      "    def meta_setup(self):\n",
+      "        TemplateNodeMixin.init(self)\n",
+      "        self.INPUT_PORT_NAME = 'in'\n",
+      "        self.OUTPUT_PORT_NAME = 'out'\n",
+      "        port_type = PortsSpecSchema.port_type\n",
+      "        self.port_inports = {\n",
+      "            self.INPUT_PORT_NAME: {\n",
+      "                port_type: [\n",
+      "                    \"pandas.DataFrame\", \"cudf.DataFrame\",\n",
+      "                    \"dask_cudf.DataFrame\", \"dask.dataframe.DataFrame\"\n",
+      "                ]\n",
+      "            },\n",
+      "        }\n",
+      "        self.port_outports = {\n",
+      "            self.OUTPUT_PORT_NAME: {\n",
+      "                port_type: \"${port:in}\"\n",
+      "            }\n",
+      "        }\n",
       "        cols_required = {\"asset\": \"int64\"}\n",
-      "        return _PortTypesMixin.meta_setup(self, required=cols_required)\n",
-      "\n",
-      "    def ports_setup(self):\n",
-      "        return _PortTypesMixin.ports_setup(self)\n",
+      "        addition = {}\n",
+      "        self.meta_inports = {\n",
+      "            self.INPUT_PORT_NAME: cols_required\n",
+      "        }\n",
+      "        self.meta_outports = {\n",
+      "            self.OUTPUT_PORT_NAME: {\n",
+      "                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,\n",
+      "                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,\n",
+      "                MetaDataSchema.META_DATA: addition\n",
+      "            }\n",
+      "        }\n",
       "\n",
       "    def conf_schema(self):\n",
       "        json = {\n",
@@ -1249,7 +1263,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 27,
+   "execution_count": 25,
    "metadata": {},
    "outputs": [
     {
@@ -1258,7 +1272,7 @@
        "{'status': 'ok', 'restart': True}"
       ]
      },
-     "execution_count": 27,
+     "execution_count": 25,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1349,7 +1363,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.5"
+   "version": "3.8.8"
   }
  },
  "nbformat": 4,
diff --git a/gQuant/plugins/gquant_plugin/notebooks/02_single_stock_trade.ipynb b/gQuant/plugins/gquant_plugin/notebooks/02_single_stock_trade.ipynb
index aa9b1d63..461d7811 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/02_single_stock_trade.ipynb
+++ b/gQuant/plugins/gquant_plugin/notebooks/02_single_stock_trade.ipynb
@@ -110,12 +110,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "205d890c5eaa49f7b8c5130b97ef8980",
+       "model_id": "948faa68bb974c82871c89396e1b0aaf",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -132,17 +132,10 @@
    "execution_count": 5,
    "metadata": {},
    "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "\n"
-     ]
-    },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "8a424b40940546e2931d8f13f3f2e74c",
+       "model_id": "31b748f68c8a4b7faf7f9d4831691f55",
        "version_major": 2,
        "version_minor": 0
       },
@@ -171,6 +164,14 @@
    "metadata": {},
    "outputs": [],
    "source": [
+    "def fig2img(fig):\n",
+    "    \"\"\"Convert a Matplotlib figure to BufferIO\"\"\"\n",
+    "    import io\n",
+    "    buf = io.BytesIO()\n",
+    "    fig.savefig(buf)\n",
+    "    buf.seek(0)\n",
+    "    return buf\n",
+    "\n",
     "def plot_figures(result):\n",
     "    # format the figures\n",
     "    figure_width = '1200px'\n",
@@ -178,22 +179,26 @@
     "    bar_figure = result['barplot.barplot']\n",
     "    sharpe_number = result['sharpe_ratio.sharpe_out']\n",
     "    cum_return = result['cumulative_return.cum_return']\n",
+    "    cum_return.set_figwidth(10)\n",
+    "    cum_return.suptitle('P & L %.3f' % (sharpe_number), fontsize=16)\n",
+    "    i = fig2img(cum_return)\n",
+    "    img_cum = widgets.Image(\n",
+    "                            value=i.read(),\n",
+    "                            format='png',\n",
+    "                            width=600,\n",
+    "                            height=900,\n",
+    "    )\n",
     "    signals = result['lineplot.lineplot']\n",
+    "    signals.set_figwidth(10)\n",
+    "    i = fig2img(signals)\n",
+    "    img_signals = widgets.Image(\n",
+    "                            value=i.read(),\n",
+    "                            format='png',\n",
+    "                            width=600,\n",
+    "                            height=900,\n",
+    "    ) \n",
     "    symbol = result['stock_selector.stock_name']\n",
-    "\n",
-    "    bar_figure.layout.height = figure_height\n",
-    "    bar_figure.layout.width = figure_width\n",
-    "    cum_return.layout.height = figure_height\n",
-    "    cum_return.layout.width = figure_width\n",
-    "    cum_return.title = 'P & L %.3f' % (sharpe_number)\n",
-    "    bar_figure.marks[0].labels = [symbol]\n",
-    "    cum_return.marks[0].labels = [symbol]\n",
-    "    signals.layout.height = figure_height\n",
-    "    signals.layout.width = figure_width\n",
-    "    bar_figure.axes = [bar_figure.axes[1]]\n",
-    "    cum_return.axes = [cum_return.axes[0]]\n",
-    "    output = widgets.VBox([bar_figure, cum_return, signals])\n",
-    "\n",
+    "    output = widgets.VBox([bar_figure, img_cum, img_signals])\n",
     "    return output"
    ]
   },
@@ -212,12 +217,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "40e6288fcd354e9eb0870f2dbfbb8608",
+       "model_id": "4637ecb627e949509f712dc524195665",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "VBox(children=(Figure(axes=[Axis(label='Price', orientation='vertical', scale=LinearScale(max=38.13, min=-10.1…"
+       "VBox(children=(Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x03 \\x00\\x00\\x02?\\x08\\x06\\x00\\x00\\x00…"
       ]
      },
      "metadata": {},
@@ -244,12 +249,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "df21e88386a2487a9f8ae6f9bbe5ac40",
+       "model_id": "c14d9769298d45fe8765a33ea56f7046",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "VBox(children=(Figure(axes=[Axis(label='Price', orientation='vertical', scale=LinearScale(max=38.13, min=-10.1…"
+       "VBox(children=(Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x03 \\x00\\x00\\x02?\\x08\\x06\\x00\\x00\\x00…"
       ]
      },
      "metadata": {},
@@ -280,7 +285,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "3e80a4f4282f4f19bf2efce27c378ff6",
+       "model_id": "cd89756e031c45c8b94aec4d2a233437",
        "version_major": 2,
        "version_minor": 0
       },
@@ -314,12 +319,7 @@
     "            replace={'stock_data': {\"load\": {'cudf_out': result['stock_data.cudf_out']}},\n",
     "                     'mean_reversion': {'conf': {'fast': para1, 'slow': para2}}})\n",
     "        figure_combo = plot_figures(o)\n",
-    "        if (len(w.children) < 2):\n",
-    "            w.children = (w.children[0], figure_combo,)\n",
-    "        else:\n",
-    "            w.children[1].children[1].marks = figure_combo.children[1].marks\n",
-    "            w.children[1].children[2].marks = figure_combo.children[2].marks\n",
-    "            w.children[1].children[1].title = 'P & L %.3f' % (o['sharpe_ratio.sharpe_out'])\n",
+    "        w.children = (w.children[0], figure_combo,)\n",
     "\n",
     "\n",
     "out = widgets.Output(layout={'border': '1px solid black'})\n",
@@ -333,6 +333,30 @@
    "cell_type": "code",
    "execution_count": 10,
    "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "ce16e6cad6754b4981b47699a5a9c669",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "Output(layout=Layout(border='1px solid black'))"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "out"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 11,
+   "metadata": {},
    "outputs": [
     {
      "data": {
@@ -340,7 +364,7 @@
        "{'status': 'ok', 'restart': True}"
       ]
      },
-     "execution_count": 10,
+     "execution_count": 11,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -375,7 +399,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.5"
+   "version": "3.8.8"
   }
  },
  "nbformat": 4,
diff --git a/gQuant/plugins/gquant_plugin/notebooks/03_simple_dask_example.ipynb b/gQuant/plugins/gquant_plugin/notebooks/03_simple_dask_example.ipynb
index 95d7eec5..2b5c5b66 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/03_simple_dask_example.ipynb
+++ b/gQuant/plugins/gquant_plugin/notebooks/03_simple_dask_example.ipynb
@@ -23,23 +23,23 @@
        "<td style=\"vertical-align: top; border: 0px solid white\">\n",
        "<h3 style=\"text-align: left;\">Client</h3>\n",
        "<ul style=\"text-align: left; list-style: none; margin: 0; padding: 0;\">\n",
-       "  <li><b>Scheduler: </b>tcp://127.0.0.1:44181</li>\n",
+       "  <li><b>Scheduler: </b>tcp://127.0.0.1:46101</li>\n",
        "  <li><b>Dashboard: </b><a href='http://127.0.0.1:8787/status' target='_blank'>http://127.0.0.1:8787/status</a></li>\n",
        "</ul>\n",
        "</td>\n",
        "<td style=\"vertical-align: top; border: 0px solid white\">\n",
        "<h3 style=\"text-align: left;\">Cluster</h3>\n",
        "<ul style=\"text-align: left; list-style:none; margin: 0; padding: 0;\">\n",
-       "  <li><b>Workers: </b>2</li>\n",
-       "  <li><b>Cores: </b>2</li>\n",
-       "  <li><b>Memory: </b>100.00 GB</li>\n",
+       "  <li><b>Workers: </b>4</li>\n",
+       "  <li><b>Cores: </b>4</li>\n",
+       "  <li><b>Memory: </b>270.39 GB</li>\n",
        "</ul>\n",
        "</td>\n",
        "</tr>\n",
        "</table>"
       ],
       "text/plain": [
-       "<Client: 'tcp://127.0.0.1:44181' processes=2 threads=2, memory=100.00 GB>"
+       "<Client: 'tcp://127.0.0.1:46101' processes=4 threads=4, memory=270.39 GB>"
       ]
      },
      "execution_count": 2,
@@ -125,12 +125,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "2d409afaf9ec46cd9d09af299b89be84",
+       "model_id": "f468dd5f7919440fad22ea7b672f82f9",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -154,54 +154,21 @@
    "cell_type": "code",
    "execution_count": 5,
    "metadata": {},
-   "outputs": [
-    {
-     "data": {
-      "application/vnd.jupyter.widget-view+json": {
-       "model_id": "ac85f4fd74e748adac67769fb68c29d3",
-       "version_major": 2,
-       "version_minor": 0
-      },
-      "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
-      ]
-     },
-     "metadata": {},
-     "output_type": "display_data"
-    }
-   ],
-   "source": [
-    "task_graph = TaskGraph.load_taskgraph('../taskgraphs/sort_stocks.gq.yaml')\n",
-    "input_cached, = task_graph.run()\n",
-    "task_graph.draw()"
-   ]
-  },
-  {
-   "cell_type": "markdown",
-   "metadata": {},
-   "source": [
-    "convert the sorted stock data into partitions and save it into csv files. Note, the data is slited in a way that the same asset belongs to the same partition"
-   ]
-  },
-  {
-   "cell_type": "code",
-   "execution_count": 6,
-   "metadata": {},
    "outputs": [
     {
      "data": {
       "text/plain": [
-       "['/home/yi/Projects/demo_greenflow_install/plugins/rapids_plugin/notebooks/many-small/0.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/plugins/rapids_plugin/notebooks/many-small/1.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/plugins/rapids_plugin/notebooks/many-small/2.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/plugins/rapids_plugin/notebooks/many-small/3.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/plugins/rapids_plugin/notebooks/many-small/4.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/plugins/rapids_plugin/notebooks/many-small/5.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/plugins/rapids_plugin/notebooks/many-small/6.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/plugins/rapids_plugin/notebooks/many-small/7.csv']"
+       "['/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/0.csv',\n",
+       " '/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/1.csv',\n",
+       " '/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/2.csv',\n",
+       " '/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/3.csv',\n",
+       " '/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/4.csv',\n",
+       " '/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/5.csv',\n",
+       " '/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/6.csv',\n",
+       " '/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/7.csv']"
       ]
      },
-     "execution_count": 6,
+     "execution_count": 5,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -211,13 +178,12 @@
     "import os\n",
     "num_partitions = 8\n",
     "\n",
-    "os.makedirs('many-small', exist_ok=True)\n",
-    "dd.from_pandas(input_cached.set_index('asset'), npartitions=num_partitions).reset_index().to_csv('many-small/*.csv', index=False)"
+    "dd.from_pandas(input_cached, npartitions=num_partitions).shuffle(on=['asset']).to_csv('many-small/*.csv', index=False)"
    ]
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 6,
    "metadata": {},
    "outputs": [
     {
@@ -268,18 +234,18 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 8,
+   "execution_count": 7,
    "metadata": {},
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "a6ff3dc1c1b84e13b42dc199059e77d5",
+       "model_id": "2124d8998aed46cebc68e3254c39deb3",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -293,23 +259,22 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 9,
+   "execution_count": 8,
    "metadata": {},
    "outputs": [
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "id:stock_data process time:0.020s\n",
-      "id:average_volume process time:0.185s\n",
-      "id:output_csv process time:0.276s\n",
-      "\n"
+      "id:stock_data process time:0.014s\n",
+      "id:average_volume process time:0.116s\n",
+      "id:output_csv process time:1.792s\n"
      ]
     },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "64b0bdf3bc584f6f93fc8c21afcdb6e3",
+       "model_id": "8ee49957eea940f9a51726d9fa27c4ed",
        "version_major": 2,
        "version_minor": 0
       },
@@ -327,7 +292,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 10,
+   "execution_count": 9,
    "metadata": {},
    "outputs": [
     {
@@ -358,28 +323,28 @@
        "  <tbody>\n",
        "    <tr>\n",
        "      <th>0</th>\n",
-       "      <td>93</td>\n",
-       "      <td>3.338977</td>\n",
+       "      <td>703638</td>\n",
+       "      <td>274.248038</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>1</th>\n",
-       "      <td>165</td>\n",
-       "      <td>3.683145</td>\n",
+       "      <td>25156</td>\n",
+       "      <td>154.621564</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>2</th>\n",
-       "      <td>239</td>\n",
-       "      <td>23.340196</td>\n",
+       "      <td>23316</td>\n",
+       "      <td>859.237660</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>3</th>\n",
-       "      <td>281</td>\n",
-       "      <td>1.332150</td>\n",
+       "      <td>705451</td>\n",
+       "      <td>92.104888</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>4</th>\n",
-       "      <td>592</td>\n",
-       "      <td>0.902251</td>\n",
+       "      <td>6405</td>\n",
+       "      <td>38.077584</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>...</th>\n",
@@ -388,28 +353,28 @@
        "    </tr>\n",
        "    <tr>\n",
        "      <th>5047</th>\n",
-       "      <td>869590</td>\n",
-       "      <td>66.575254</td>\n",
+       "      <td>4716</td>\n",
+       "      <td>6.122072</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>5048</th>\n",
-       "      <td>869591</td>\n",
-       "      <td>1.928534</td>\n",
+       "      <td>680831</td>\n",
+       "      <td>96.094879</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>5049</th>\n",
-       "      <td>869592</td>\n",
-       "      <td>56.085032</td>\n",
+       "      <td>708156</td>\n",
+       "      <td>2017.879479</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>5050</th>\n",
-       "      <td>869597</td>\n",
-       "      <td>44.179514</td>\n",
+       "      <td>24991</td>\n",
+       "      <td>27.420712</td>\n",
        "    </tr>\n",
        "    <tr>\n",
        "      <th>5051</th>\n",
-       "      <td>869599</td>\n",
-       "      <td>20.089748</td>\n",
+       "      <td>869301</td>\n",
+       "      <td>68.514029</td>\n",
        "    </tr>\n",
        "  </tbody>\n",
        "</table>\n",
@@ -417,23 +382,23 @@
        "</div>"
       ],
       "text/plain": [
-       "       asset     volume\n",
-       "0         93   3.338977\n",
-       "1        165   3.683145\n",
-       "2        239  23.340196\n",
-       "3        281   1.332150\n",
-       "4        592   0.902251\n",
-       "...      ...        ...\n",
-       "5047  869590  66.575254\n",
-       "5048  869591   1.928534\n",
-       "5049  869592  56.085032\n",
-       "5050  869597  44.179514\n",
-       "5051  869599  20.089748\n",
+       "       asset       volume\n",
+       "0     703638   274.248038\n",
+       "1      25156   154.621564\n",
+       "2      23316   859.237660\n",
+       "3     705451    92.104888\n",
+       "4       6405    38.077584\n",
+       "...      ...          ...\n",
+       "5047    4716     6.122072\n",
+       "5048  680831    96.094879\n",
+       "5049  708156  2017.879479\n",
+       "5050   24991    27.420712\n",
+       "5051  869301    68.514029\n",
        "\n",
        "[5052 rows x 2 columns]"
       ]
      },
-     "execution_count": 10,
+     "execution_count": 9,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -451,7 +416,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 11,
+   "execution_count": 10,
    "metadata": {},
    "outputs": [],
    "source": [
@@ -460,7 +425,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 12,
+   "execution_count": 11,
    "metadata": {},
    "outputs": [
     {
@@ -469,7 +434,7 @@
        "{'status': 'ok', 'restart': True}"
       ]
      },
-     "execution_count": 12,
+     "execution_count": 11,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -511,7 +476,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.5"
+   "version": "3.8.8"
   }
  },
  "nbformat": 4,
diff --git a/gQuant/plugins/gquant_plugin/notebooks/04_portfolio_trade.ipynb b/gQuant/plugins/gquant_plugin/notebooks/04_portfolio_trade.ipynb
index 8c8d28a0..3dd78f7d 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/04_portfolio_trade.ipynb
+++ b/gQuant/plugins/gquant_plugin/notebooks/04_portfolio_trade.ipynb
@@ -80,7 +80,7 @@
        "<td style=\"vertical-align: top; border: 0px solid white\">\n",
        "<h3 style=\"text-align: left;\">Client</h3>\n",
        "<ul style=\"text-align: left; list-style: none; margin: 0; padding: 0;\">\n",
-       "  <li><b>Scheduler: </b>tcp://127.0.0.1:39757</li>\n",
+       "  <li><b>Scheduler: </b>tcp://127.0.0.1:43453</li>\n",
        "  <li><b>Dashboard: </b><a href='http://127.0.0.1:8787/status' target='_blank'>http://127.0.0.1:8787/status</a></li>\n",
        "</ul>\n",
        "</td>\n",
@@ -89,14 +89,14 @@
        "<ul style=\"text-align: left; list-style:none; margin: 0; padding: 0;\">\n",
        "  <li><b>Workers: </b>2</li>\n",
        "  <li><b>Cores: </b>2</li>\n",
-       "  <li><b>Memory: </b>100.00 GB</li>\n",
+       "  <li><b>Memory: </b>93.13 GiB</li>\n",
        "</ul>\n",
        "</td>\n",
        "</tr>\n",
        "</table>"
       ],
       "text/plain": [
-       "<Client: 'tcp://127.0.0.1:39757' processes=2 threads=2, memory=100.00 GB>"
+       "<Client: 'tcp://127.0.0.1:43453' processes=2 threads=2, memory=93.13 GiB>"
       ]
      },
      "execution_count": 2,
@@ -133,12 +133,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "61924f0cdef24365b2086fdafc7b5acb",
+       "model_id": "392c86b9189e4d34a0ccc15f0c84d3c2",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -168,14 +168,14 @@
     {
      "data": {
       "text/plain": [
-       "['/home/yi/Projects/demo_greenflow_install/notebooks/many-small/0.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/notebooks/many-small/1.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/notebooks/many-small/2.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/notebooks/many-small/3.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/notebooks/many-small/4.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/notebooks/many-small/5.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/notebooks/many-small/6.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/notebooks/many-small/7.csv']"
+       "['/home/yi/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/0.csv',\n",
+       " '/home/yi/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/1.csv',\n",
+       " '/home/yi/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/2.csv',\n",
+       " '/home/yi/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/3.csv',\n",
+       " '/home/yi/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/4.csv',\n",
+       " '/home/yi/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/5.csv',\n",
+       " '/home/yi/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/6.csv',\n",
+       " '/home/yi/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/7.csv']"
       ]
      },
      "execution_count": 4,
@@ -188,8 +188,7 @@
     "import os\n",
     "num_partitions = 8\n",
     "\n",
-    "os.makedirs('many-small', exist_ok=True)\n",
-    "dd.from_pandas(input_cached.set_index('asset'), npartitions=num_partitions).reset_index().to_csv('many-small/*.csv', index=False)"
+    "dd.from_pandas(input_cached, npartitions=num_partitions).shuffle(on=['asset']).to_csv('many-small/*.csv', index=False)"
    ]
   },
   {
@@ -206,13 +205,13 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 5,
    "metadata": {},
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "1684faac504943b287881bfe436fd78c",
+       "model_id": "396fe7e9619b4a8ca09ae897c996ed25",
        "version_major": 2,
        "version_minor": 0
       },
@@ -275,7 +274,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 6,
    "metadata": {},
    "outputs": [
     {
@@ -325,12 +324,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "c91e0a7be3bf4e69973965709e5eff9d",
+       "model_id": "0b588cf73c9e48fd84ba6e8be95ddd2a",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -357,12 +356,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "6ce4096ee9dd4ec78d929077650a9899",
+       "model_id": "262dcc5b45c1439a9cae89f087388453",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -442,19 +441,59 @@
    "outputs": [],
    "source": [
     "from greenflow.dataframe_flow import Node\n",
-    "from greenflow_gquant_plugin._port_type_node import _PortTypesMixin\n",
+    "from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin\n",
     "from greenflow.dataframe_flow.portsSpecSchema import ConfSchema\n",
     "\n",
     "\n",
-    "class MaxNode(Node, _PortTypesMixin):\n",
+    "class MaxNode(TemplateNodeMixin, Node):\n",
     "\n",
     "    def init(self):\n",
-    "        _PortTypesMixin.init(self)\n",
+    "        TemplateNodeMixin.init(self)\n",
     "        self.INPUT_PORT_NAME = 'in'\n",
     "        self.OUTPUT_PORT_NAME = 'out'\n",
+    "        port_type = PortsSpecSchema.port_type\n",
+    "        port_inports = {\n",
+    "            self.INPUT_PORT_NAME: {\n",
+    "                port_type: [\n",
+    "                    \"pandas.DataFrame\", \"cudf.DataFrame\",\n",
+    "                    \"dask_cudf.DataFrame\", \"dask.dataframe.DataFrame\"\n",
+    "                ]\n",
+    "            },\n",
+    "        }\n",
+    "        port_outports = {\n",
+    "            self.OUTPUT_PORT_NAME: {\n",
+    "                port_type: \"${port:in}\"\n",
+    "            }\n",
+    "        }\n",
+    "        cols_required = {\"asset\": \"int64\"}\n",
+    "        if 'column' in self.conf:\n",
+    "            retention = {self.conf['column']: \"float64\",\n",
+    "                         \"asset\": \"int64\"}\n",
+    "        else:\n",
+    "            retention = {\"asset\": \"int64\"}\n",
+    "        meta_inports = {\n",
+    "            self.INPUT_PORT_NAME: cols_required\n",
+    "        }\n",
+    "        meta_outports = {\n",
+    "            self.OUTPUT_PORT_NAME: {\n",
+    "                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,\n",
+    "                MetaDataSchema.META_DATA: retention\n",
+    "            }\n",
+    "        }\n",
+    "        self.template_ports_setup(\n",
+    "            in_ports=port_inports,\n",
+    "            out_ports=port_outports\n",
+    "        )\n",
+    "        self.template_meta_setup(\n",
+    "            in_ports=meta_inports,\n",
+    "            out_ports=meta_outports\n",
+    "        )\n",
     "\n",
     "    def ports_setup(self):\n",
-    "        return _PortTypesMixin.ports_setup(self)\n",
+    "        return TemplateNodeMixin.ports_setup(self)\n",
+    "\n",
+    "    def meta_setup(self):\n",
+    "        return TemplateNodeMixin.meta_setup(self)\n",
     "\n",
     "    def conf_schema(self):\n",
     "        json = {\n",
@@ -500,21 +539,7 @@
     "        volume_df = input_df[[max_column,\n",
     "                              \"asset\"]].groupby([\"asset\"]).max().reset_index()\n",
     "        volume_df.columns = ['asset', max_column]\n",
-    "        return {self.OUTPUT_PORT_NAME: volume_df}\n",
-    "\n",
-    "    def meta_setup(self):\n",
-    "        cols_required = {\"asset\": \"int64\"}\n",
-    "        if 'column' in self.conf:\n",
-    "            retention = {self.conf['column']: \"float64\",\n",
-    "                         \"asset\": \"int64\"}\n",
-    "            return _PortTypesMixin.retention_meta_setup(self,\n",
-    "                                                        retention,\n",
-    "                                                        required=cols_required)\n",
-    "        else:\n",
-    "            retention = {\"asset\": \"int64\"}\n",
-    "            return _PortTypesMixin.retention_meta_setup(self,\n",
-    "                                                        retention,\n",
-    "                                                        required=cols_required)"
+    "        return {self.OUTPUT_PORT_NAME: volume_df}"
    ]
   },
   {
@@ -547,14 +572,14 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "id:stock_data process time:4.120s\n",
-      "id:preprocess process time:0.879s\n",
-      "id:sort_after process time:0.055s\n",
-      "id:exp_mean_reversion process time:0.796s\n",
+      "id:stock_data process time:4.001s\n",
+      "id:preprocess process time:0.945s\n",
+      "id:sort_after process time:0.059s\n",
+      "id:exp_mean_reversion process time:0.768s\n",
       "id:backtest process time:0.001s\n",
       "id:portfolio_opt process time:0.012s\n",
       "id:sharpe_ratio process time:0.001s\n",
-      "id:cumulative_return process time:0.016s\n"
+      "id:cumulative_return process time:0.020s\n"
      ]
     }
    ],
@@ -609,17 +634,10 @@
    "execution_count": 12,
    "metadata": {},
    "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "\n"
-     ]
-    },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "ca90df245c254e4ba78ae8ac9bd404ed",
+       "model_id": "96c40517c40f485aba41c9c5714588f5",
        "version_major": 2,
        "version_minor": 0
       },
@@ -652,30 +670,25 @@
    "outputs": [
     {
      "data": {
-      "application/vnd.jupyter.widget-view+json": {
-       "model_id": "dc0336888fa940d4903d91ce87244888",
-       "version_major": 2,
-       "version_minor": 0
-      },
+      "image/png": "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\n",
       "text/plain": [
-       "Figure(axes=[Axis(label='Cumulative return', orientation='vertical', scale=LinearScale(), side='left'), Axis(l…"
+       "<Figure size 720x288 with 1 Axes>"
       ]
      },
+     "execution_count": 13,
      "metadata": {},
-     "output_type": "display_data"
+     "output_type": "execute_result"
     }
    ],
    "source": [
     "# define the function to format the plots\n",
     "def plot_figures(outputs):\n",
     "    # format the figures\n",
-    "    figure_width = '1200px'\n",
-    "    figure_height = '400px'\n",
-    "    sharpe_number = outputs[0]\n",
-    "    cum_return = outputs[1]\n",
-    "    cum_return.layout.height = figure_height\n",
-    "    cum_return.layout.width = figure_width\n",
-    "    cum_return.title = 'P & L %.3f' % (sharpe_number)\n",
+    "    \n",
+    "    sharpe_number = outputs['sharpe_ratio.sharpe_out']\n",
+    "    cum_return = outputs['cumulative_return.cum_return']\n",
+    "    cum_return.set_figwidth(10)\n",
+    "    cum_return.suptitle('P & L %.3f' % (sharpe_number), fontsize=16)\n",
     "    return cum_return\n",
     "\n",
     "plot_figures(o_gpu)"
@@ -711,33 +724,30 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "id:stock_data process time:4.021s\n",
-      "id:preprocess process time:0.702s\n",
-      "id:sort_after process time:0.130s\n",
-      "id:exp_mean_reversion process time:0.079s\n",
-      "id:backtest process time:0.003s\n",
-      "id:portfolio_opt process time:0.017s\n",
+      "id:stock_data process time:3.993s\n",
+      "id:preprocess process time:0.676s\n",
+      "id:sort_after process time:0.142s\n",
+      "id:exp_mean_reversion process time:0.083s\n",
+      "id:backtest process time:0.002s\n",
+      "id:portfolio_opt process time:0.021s\n",
       "id:sharpe_ratio process time:0.001s\n",
-      "id:cumulative_return process time:0.015s\n",
+      "id:cumulative_return process time:0.020s\n",
       "5052 stocks in original dataset.\n",
       "4405 stocks remaining after filtering.\n",
-      "CPU times: user 4.82 s, sys: 725 ms, total: 5.54 s\n",
-      "Wall time: 5.36 s\n"
+      "CPU times: user 4.49 s, sys: 780 ms, total: 5.27 s\n",
+      "Wall time: 5.06 s\n"
      ]
     },
     {
      "data": {
-      "application/vnd.jupyter.widget-view+json": {
-       "model_id": "4227850521c94892a4fe3c2b94183811",
-       "version_major": 2,
-       "version_minor": 0
-      },
+      "image/png": "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\n",
       "text/plain": [
-       "Figure(axes=[Axis(label='Cumulative return', orientation='vertical', scale=LinearScale()), Axis(label='Time', …"
+       "<Figure size 720x288 with 1 Axes>"
       ]
      },
+     "execution_count": 15,
      "metadata": {},
-     "output_type": "display_data"
+     "output_type": "execute_result"
     }
    ],
    "source": [
@@ -790,16 +800,16 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "id:stock_data process time:64.310s\n",
-      "id:preprocess process time:22.999s\n",
-      "id:sort_after process time:1.303s\n",
-      "id:exp_mean_reversion process time:6.645s\n",
+      "id:stock_data process time:64.450s\n",
+      "id:preprocess process time:22.882s\n",
+      "id:sort_after process time:1.386s\n",
+      "id:exp_mean_reversion process time:7.450s\n",
       "id:backtest process time:0.043s\n",
-      "id:portfolio_opt process time:0.448s\n",
+      "id:portfolio_opt process time:0.462s\n",
       "id:sharpe_ratio process time:0.001s\n",
-      "id:cumulative_return process time:0.014s\n",
-      "CPU times: user 1min 23s, sys: 14.5 s, total: 1min 38s\n",
-      "Wall time: 1min 35s\n"
+      "id:cumulative_return process time:0.018s\n",
+      "CPU times: user 1min 25s, sys: 14.3 s, total: 1min 39s\n",
+      "Wall time: 1min 36s\n"
      ]
     }
    ],
@@ -813,22 +823,19 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 18,
+   "execution_count": 17,
    "metadata": {},
    "outputs": [
     {
      "data": {
-      "application/vnd.jupyter.widget-view+json": {
-       "model_id": "8e2c4dadba0148d5b9294a42ec0689a3",
-       "version_major": 2,
-       "version_minor": 0
-      },
+      "image/png": "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\n",
       "text/plain": [
-       "Figure(axes=[Axis(label='Cumulative return', orientation='vertical', scale=LinearScale(), side='left'), Axis(l…"
+       "<Figure size 720x288 with 1 Axes>"
       ]
      },
+     "execution_count": 17,
      "metadata": {},
-     "output_type": "display_data"
+     "output_type": "execute_result"
     }
    ],
    "source": [
@@ -860,36 +867,33 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 19,
+   "execution_count": 18,
    "metadata": {},
    "outputs": [
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "id:stock_data process time:0.115s\n",
-      "id:preprocess process time:7.280s\n",
-      "id:backtest process time:0.013s\n",
-      "id:portfolio_opt process time:0.036s\n",
-      "id:sharpe_ratio process time:0.328s\n",
-      "id:cumulative_return process time:0.353s\n",
-      "CPU times: user 3.85 s, sys: 377 ms, total: 4.22 s\n",
-      "Wall time: 14.4 s\n"
+      "id:stock_data process time:0.116s\n",
+      "id:preprocess process time:0.249s\n",
+      "id:backtest process time:0.006s\n",
+      "id:portfolio_opt process time:0.015s\n",
+      "id:sharpe_ratio process time:4.481s\n",
+      "id:cumulative_return process time:1.667s\n",
+      "CPU times: user 1.09 s, sys: 190 ms, total: 1.27 s\n",
+      "Wall time: 6.6 s\n"
      ]
     },
     {
      "data": {
-      "application/vnd.jupyter.widget-view+json": {
-       "model_id": "abf0fa6953074fad9f9bd7e7fa08872a",
-       "version_major": 2,
-       "version_minor": 0
-      },
+      "image/png": "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\n",
       "text/plain": [
-       "Figure(axes=[Axis(label='Cumulative return', orientation='vertical', scale=LinearScale()), Axis(label='Time', …"
+       "<Figure size 720x288 with 1 Axes>"
       ]
      },
+     "execution_count": 18,
      "metadata": {},
-     "output_type": "display_data"
+     "output_type": "execute_result"
     }
    ],
    "source": [
@@ -927,13 +931,13 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 20,
+   "execution_count": 19,
    "metadata": {},
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "30f5d401388b40648ce422e3973f4d9f",
+       "model_id": "242b46f7c9544725a3b64a13d8556275",
        "version_major": 2,
        "version_minor": 0
       },
@@ -948,6 +952,14 @@
    "source": [
     "import ipywidgets as widgets\n",
     "\n",
+    "def fig2img(fig):\n",
+    "    \"\"\"Convert a Matplotlib figure to BufferIO\"\"\"\n",
+    "    import io\n",
+    "    buf = io.BytesIO()\n",
+    "    fig.savefig(buf)\n",
+    "    buf.seek(0)\n",
+    "    return buf\n",
+    "    \n",
     "para_selector = widgets.IntRangeSlider(value=[10, 30],\n",
     "                                       min=3,\n",
     "                                       max=60,\n",
@@ -982,7 +994,14 @@
     "                                    }}})\n",
     "\n",
     "        figure_combo = plot_figures(o)\n",
-    "        w.children = (w.children[0], figure_combo,)\n",
+    "        i = fig2img(figure_combo)\n",
+    "        img = widgets.Image(\n",
+    "            value=i.read(),\n",
+    "            format='png',\n",
+    "            width=600,\n",
+    "            height=900,\n",
+    "        )\n",
+    "        w.children = (w.children[0], img,)\n",
     "\n",
     "\n",
     "out = widgets.Output(layout={'border': '1px solid black'})\n",
@@ -994,13 +1013,13 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 21,
+   "execution_count": 20,
    "metadata": {},
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "2b093c4f26244fdf831e4b1507dec36a",
+       "model_id": "091b5a654b5a47988529c857511948c0",
        "version_major": 2,
        "version_minor": 0
       },
@@ -1018,7 +1037,7 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 22,
+   "execution_count": 21,
    "metadata": {},
    "outputs": [
     {
@@ -1027,7 +1046,7 @@
        "{'status': 'ok', 'restart': True}"
       ]
      },
-     "execution_count": 22,
+     "execution_count": 21,
      "metadata": {},
      "output_type": "execute_result"
     }
@@ -1038,6 +1057,104 @@
     "app.kernel.do_shutdown(True)"
    ]
   },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  },
   {
    "cell_type": "code",
    "execution_count": null,
@@ -1062,7 +1179,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.5"
+   "version": "3.8.8"
   }
  },
  "nbformat": 4,
diff --git a/gQuant/plugins/gquant_plugin/notebooks/05_customize_nodes_with_ports.ipynb b/gQuant/plugins/gquant_plugin/notebooks/05_customize_nodes_with_ports.ipynb
index bdf87ad2..e0f225dc 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/05_customize_nodes_with_ports.ipynb
+++ b/gQuant/plugins/gquant_plugin/notebooks/05_customize_nodes_with_ports.ipynb
@@ -224,14 +224,14 @@
     "        input_meta = self.get_input_meta()\n",
     "        output_cols = ({\n",
     "                'distance_df': {\n",
-    "                    'distance_cudf': 'float64',\n",
     "                    'x': 'float64',\n",
-    "                    'y': 'float64'\n",
+    "                    'y': 'float64',\n",
+    "                    'distance_cudf': 'float64',\n",
     "                },\n",
     "                'distance_abs_df': {\n",
-    "                    'distance_abs_cudf': 'float64',\n",
     "                    'x': 'float64',\n",
-    "                    'y': 'float64'\n",
+    "                    'y': 'float64',\n",
+    "                    'distance_abs_cudf': 'float64',\n",
     "                }\n",
     "            })\n",
     "        if 'points_df_in' in input_meta:\n",
@@ -319,7 +319,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "3a1e2a3eb1394ce3bcab13db4daf6ec9",
+       "model_id": "3804fbdf27b848cf91ea952d3cdd4940",
        "version_major": 2,
        "version_minor": 0
       },
@@ -351,12 +351,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "85fd29bfb68c4dcdabf1b5fe2aa604be",
+       "model_id": "36a928bd508c4c4ca0524877a0f81c8c",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'points_task'), ('type', 'PointNode'), ('conf', {'npts': 1…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'points_task'), ('type', 'PointNode'), ('conf', {'npts'…"
       ]
      },
      "metadata": {},
@@ -379,17 +379,10 @@
    "execution_count": 9,
    "metadata": {},
    "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "\n"
-     ]
-    },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "119fc64a511742dfb9b2db98ed120971",
+       "model_id": "3e0b5f8ad7d440008dfc21ae8b68c9b5",
        "version_major": 2,
        "version_minor": 0
       },
@@ -486,9 +479,9 @@
     "        input_meta = self.get_input_meta()\n",
     "        output_cols = ({\n",
     "                'distance_df': {\n",
-    "                    'distance_numba': 'float64',\n",
     "                    'x': 'float64',\n",
-    "                    'y': 'float64'\n",
+    "                    'y': 'float64',\n",
+    "                    'distance_numba': 'float64',\n",
     "                }\n",
     "            })\n",
     "        if 'points_df_in' in input_meta:\n",
@@ -599,9 +592,9 @@
     "        input_meta = self.get_input_meta()\n",
     "        output_cols = ({\n",
     "                'distance_df': {\n",
-    "                    'distance_cupy': 'float64',\n",
     "                    'x': 'float64',\n",
-    "                    'y': 'float64'\n",
+    "                    'y': 'float64',\n",
+    "                    'distance_cupy': 'float64',\n",
     "                }\n",
     "            })\n",
     "        if 'points_df_in' in input_meta:\n",
@@ -667,12 +660,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "0b49a2e2b8d9437cabe925588839a29a",
+       "model_id": "b2938a58d6094658a26096f6afa519af",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'points_task'), ('type', 'PointNode'), ('conf', {'npts': 1…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'points_task'), ('type', 'PointNode'), ('conf', {'npts'…"
       ]
      },
      "metadata": {},
@@ -738,17 +731,10 @@
    "execution_count": 14,
    "metadata": {},
    "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "\n"
-     ]
-    },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "d4014aaf33944b86a6618a2d6ecc127e",
+       "model_id": "9e9673e8b4134420821bf09612a88727",
        "version_major": 2,
        "version_minor": 0
       },
@@ -924,12 +910,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "ecb7023af0084c43b953f33c03a12cf0",
+       "model_id": "c49345629c46447e9968f9fb1aff9612",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'points_task'), ('type', 'PointNode'), ('conf', {'npts': 1…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'points_task'), ('type', 'PointNode'), ('conf', {'npts'…"
       ]
      },
      "metadata": {},
@@ -1034,23 +1020,23 @@
        "<td style=\"vertical-align: top; border: 0px solid white\">\n",
        "<h3 style=\"text-align: left;\">Client</h3>\n",
        "<ul style=\"text-align: left; list-style: none; margin: 0; padding: 0;\">\n",
-       "  <li><b>Scheduler: </b>tcp://127.0.0.1:36761</li>\n",
+       "  <li><b>Scheduler: </b>tcp://127.0.0.1:45755</li>\n",
        "  <li><b>Dashboard: </b><a href='http://127.0.0.1:8787/status' target='_blank'>http://127.0.0.1:8787/status</a></li>\n",
        "</ul>\n",
        "</td>\n",
        "<td style=\"vertical-align: top; border: 0px solid white\">\n",
        "<h3 style=\"text-align: left;\">Cluster</h3>\n",
        "<ul style=\"text-align: left; list-style:none; margin: 0; padding: 0;\">\n",
-       "  <li><b>Workers: </b>2</li>\n",
-       "  <li><b>Cores: </b>2</li>\n",
-       "  <li><b>Memory: </b>100.00 GB</li>\n",
+       "  <li><b>Workers: </b>4</li>\n",
+       "  <li><b>Cores: </b>4</li>\n",
+       "  <li><b>Memory: </b>270.39 GB</li>\n",
        "</ul>\n",
        "</td>\n",
        "</tr>\n",
        "</table>"
       ],
       "text/plain": [
-       "<Client: 'tcp://127.0.0.1:36761' processes=2 threads=2, memory=100.00 GB>"
+       "<Client: 'tcp://127.0.0.1:45755' processes=4 threads=4, memory=270.39 GB>"
       ]
      },
      "execution_count": 20,
@@ -1186,12 +1172,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "261bd245de244b109984bc369777f922",
+       "model_id": "2a65b19cca1d42d0820c0c9468e63c6f",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'points_task'), ('type', 'PointNode'), ('conf', {'npts': 1…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'points_task'), ('type', 'PointNode'), ('conf', {'npts'…"
       ]
      },
      "metadata": {},
@@ -1242,15 +1228,27 @@
     "    }\n",
     "}\n",
     "\n",
+    "dask_compute_tspec = {\n",
+    "    TaskSpecSchema.task_id: 'dask_compute',\n",
+    "    TaskSpecSchema.node_type: \"DaskComputeNode\",\n",
+    "    TaskSpecSchema.conf: {},\n",
+    "    TaskSpecSchema.module: \"greenflow_dask_plugin\",\n",
+    "    TaskSpecSchema.inputs: {\n",
+    "        'distance_by_cudf@distance_df': 'distance_by_cudf.distance_df',\n",
+    "        'distance_by_numba@distance_df': 'distance_by_numba.distance_df',\n",
+    "        'distance_by_cupy@distance_df': 'distance_by_cupy.distance_df'\n",
+    "    }\n",
+    "}\n",
+    "\n",
     "out_spec = {\n",
     "    TaskSpecSchema.task_id: '',\n",
     "    TaskSpecSchema.node_type: \"Output_Collector\",\n",
     "    TaskSpecSchema.conf: {},\n",
     "    TaskSpecSchema.inputs: {\n",
     "        'in0': 'distributed_points.points_ddf_out',\n",
-    "        'in1': 'distance_by_cudf.distance_df',\n",
-    "        'in2': 'distance_by_numba.distance_df',\n",
-    "        'in3': 'distance_by_cupy.distance_df'\n",
+    "        'in1': 'dask_compute.distance_by_cudf@distance_df',\n",
+    "        'in2': 'dask_compute.distance_by_numba@distance_df',\n",
+    "        'in3': 'dask_compute.distance_by_cupy@distance_df'\n",
     "    }\n",
     "}\n",
     "\n",
@@ -1261,6 +1259,7 @@
     "    dask_cudf_distance_tspec,\n",
     "    dask_numba_distance_tspec,\n",
     "    dask_cupy_distance_tspec,\n",
+    "    dask_compute_tspec,\n",
     "    out_spec\n",
     "]\n",
     "\n",
@@ -1283,15 +1282,12 @@
    "source": [
     "out_list = [\n",
     "    'distributed_points.points_ddf_out',\n",
-    "    'distance_by_cudf.distance_df',\n",
-    "    'distance_by_numba.distance_df',\n",
-    "    'distance_by_cupy.distance_df'\n",
+    "    'dask_compute.distance_by_cudf@distance_df',\n",
+    "    'dask_compute.distance_by_numba@distance_df',\n",
+    "    'dask_compute.distance_by_cupy@distance_df'\n",
     "]\n",
     "\n",
-    "(points_ddf, ddf_w_cudf, ddf_w_numba, ddf_w_cupy) = task_graph.run(out_list)\n",
-    "df_w_cudf = ddf_w_cudf.compute()\n",
-    "df_w_numba = ddf_w_numba.compute()\n",
-    "df_w_cupy = ddf_w_cupy.compute()"
+    "(points_ddf, df_w_cudf, df_w_numba, df_w_cupy) = task_graph.run(out_list)\n"
    ]
   },
   {
@@ -1302,12 +1298,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "261bd245de244b109984bc369777f922",
+       "model_id": "2a65b19cca1d42d0820c0c9468e63c6f",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(cache={'nodes': [{'width': 110, 'id': 'points_task', 'type': 'PointNode', 'schema': {'title': 'Po…"
+       "GreenflowWidget(cache={'nodes': [{'width': 110, 'id': 'points_task', 'type': 'PointNode', 'schema': {'title': …"
       ]
      },
      "metadata": {},
@@ -1315,8 +1311,34 @@
     }
    ],
    "source": [
-    "verify_cudf_numba_tspec = verify_tspec.copy()\n",
-    "verify_cudf_cupy_tspec = verify_tspec2.copy()\n",
+    "\n",
+    "verify_cudf_numba_tspec = {\n",
+    "    TaskSpecSchema.task_id: 'verify_cudf_to_numba',\n",
+    "    TaskSpecSchema.node_type: VerifyNode,\n",
+    "    TaskSpecSchema.conf: {\n",
+    "        'df1_col': 'distance_cudf',\n",
+    "        'df2_col': 'distance_numba'\n",
+    "    },    \n",
+    "    TaskSpecSchema.module: module_name,\n",
+    "    TaskSpecSchema.inputs: {\n",
+    "        'df1': 'dask_compute.distance_by_cudf@distance_df',\n",
+    "        'df2': 'dask_compute.distance_by_numba@distance_df'\n",
+    "    }\n",
+    "}\n",
+    "\n",
+    "verify_cudf_cupy_tspec = {\n",
+    "    TaskSpecSchema.task_id: 'verify_cudf_to_cupy',\n",
+    "    TaskSpecSchema.node_type: VerifyNode,\n",
+    "    TaskSpecSchema.conf: {\n",
+    "        'df1_col': 'distance_cudf',\n",
+    "        'df2_col': 'distance_cupy'\n",
+    "    },\n",
+    "    TaskSpecSchema.module: module_name,\n",
+    "    TaskSpecSchema.inputs: {\n",
+    "        'df1': 'dask_compute.distance_by_cudf@distance_df',\n",
+    "        'df2': 'dask_compute.distance_by_cupy@distance_df'\n",
+    "    }\n",
+    "}\n",
     "\n",
     "task_graph.extend(\n",
     "    [verify_cudf_numba_tspec,\n",
@@ -1341,37 +1363,29 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "HEAD points_ddf:\n",
-      "          x         y\n",
-      "0  0.793239  0.794183\n",
-      "1  0.405215  0.753843\n",
-      "2  0.039310  0.076363\n",
-      "3  0.456579  0.369289\n",
-      "4  0.165980  0.383398\n",
-      "\n",
       "HEAD df_w_cudf:\n",
       "          x         y  distance_cudf\n",
-      "0  0.793239  0.794183       1.122477\n",
-      "1  0.405215  0.753843       0.855849\n",
-      "2  0.039310  0.076363       0.085887\n",
-      "3  0.456579  0.369289       0.587230\n",
-      "4  0.165980  0.383398       0.417784\n",
+      "0  0.751644  0.814127       1.108048\n",
+      "1  0.561972  0.888258       1.051102\n",
+      "2  0.031557  0.593508       0.594347\n",
+      "3  0.520960  0.634597       0.821044\n",
+      "4  0.234062  0.574185       0.620059\n",
       "\n",
       "HEAD df_w_numba:\n",
       "          x         y  distance_numba\n",
-      "0  0.793239  0.794183        1.122477\n",
-      "1  0.405215  0.753843        0.855849\n",
-      "2  0.039310  0.076363        0.085887\n",
-      "3  0.456579  0.369289        0.587230\n",
-      "4  0.165980  0.383398        0.417784\n",
+      "0  0.751644  0.814127        1.108048\n",
+      "1  0.561972  0.888258        1.051102\n",
+      "2  0.031557  0.593508        0.594347\n",
+      "3  0.520960  0.634597        0.821044\n",
+      "4  0.234062  0.574185        0.620059\n",
       "\n",
       "HEAD df_w_cupy:\n",
       "          x         y  distance_cupy\n",
-      "0  0.793239  0.794183       1.122477\n",
-      "1  0.405215  0.753843       0.855849\n",
-      "2  0.039310  0.076363       0.085887\n",
-      "3  0.456579  0.369289       0.587230\n",
-      "4  0.165980  0.383398       0.417784\n",
+      "0  0.751644  0.814127       1.108048\n",
+      "1  0.561972  0.888258       1.051102\n",
+      "2  0.031557  0.593508       0.594347\n",
+      "3  0.520960  0.634597       0.821044\n",
+      "4  0.234062  0.574185       0.620059\n",
       "\n",
       "Max Difference cudf to numba: 2.220446049250313e-16\n",
       "Max Difference cudf to cupy: 2.220446049250313e-16\n"
@@ -1379,35 +1393,14 @@
     }
    ],
    "source": [
-    "# Use results above and avoid re-running dask\n",
-    "replace_spec = {\n",
-    "    'distance_by_cudf': {\n",
-    "        TaskSpecSchema.load: {\n",
-    "            'distance_df': ddf_w_cudf\n",
-    "        }\n",
-    "    },\n",
-    "    'distance_by_numba': {\n",
-    "        TaskSpecSchema.load: {\n",
-    "            'distance_df': ddf_w_numba\n",
-    "        }\n",
-    "    },\n",
-    "    'distance_by_cupy': {\n",
-    "        TaskSpecSchema.load: {\n",
-    "            'distance_df': ddf_w_cupy\n",
-    "        }\n",
-    "    }\n",
-    "}\n",
-    "\n",
     "(max_cudf_to_numba_diff, max_cudf_to_cupy_diff) = task_graph.run(\n",
     "    ['verify_cudf_to_numba.max_diff',\n",
-    "     'verify_cudf_to_cupy.max_diff'],\n",
-    "    replace=replace_spec\n",
+    "     'verify_cudf_to_cupy.max_diff']\n",
     ")\n",
     "\n",
-    "print('HEAD points_ddf:\\n{}\\n'.format(points_ddf.head()))\n",
-    "print('HEAD df_w_cudf:\\n{}\\n'.format(ddf_w_cudf.head()))\n",
-    "print('HEAD df_w_numba:\\n{}\\n'.format(ddf_w_numba.head()))\n",
-    "print('HEAD df_w_cupy:\\n{}\\n'.format(ddf_w_cupy.head()))\n",
+    "print('HEAD df_w_cudf:\\n{}\\n'.format(df_w_cudf.head()))\n",
+    "print('HEAD df_w_numba:\\n{}\\n'.format(df_w_numba.head()))\n",
+    "print('HEAD df_w_cupy:\\n{}\\n'.format(df_w_cupy.head()))\n",
     "print('Max Difference cudf to numba: {}'.format(max_cudf_to_numba_diff))\n",
     "print('Max Difference cudf to cupy: {}'.format(max_cudf_to_cupy_diff))\n"
    ]
@@ -1440,12 +1433,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "340070cd3cf9451fbf61f19052b3d66c",
+       "model_id": "980cd680a2bc4de19097d322c60c7c9b",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'points_task'), ('type', 'PointNode'), ('conf', {'npts': 1…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'points_task'), ('type', 'PointNode'), ('conf', {'npts'…"
       ]
      },
      "metadata": {},
@@ -1556,27 +1549,27 @@
      "text": [
       "HEAD df_w_cudf:\n",
       "          x         y  distance_cudf\n",
-      "0  0.729991  0.418487       0.841438\n",
-      "1  0.887041  0.875456       1.246301\n",
-      "2  0.763563  0.220629       0.794799\n",
-      "3  0.454909  0.976449       1.077217\n",
-      "4  0.108439  0.707618       0.715878\n",
+      "0  0.556184  0.413430       0.693012\n",
+      "1  0.287546  0.091868       0.301865\n",
+      "2  0.544135  0.827519       0.990389\n",
+      "3  0.189693  0.775072       0.797947\n",
+      "4  0.961586  0.752678       1.221135\n",
       "\n",
       "HEAD df_w_numba:\n",
       "          x         y  distance_numba\n",
-      "0  0.729991  0.418487        0.841438\n",
-      "1  0.887041  0.875456        1.246301\n",
-      "2  0.763563  0.220629        0.794799\n",
-      "3  0.454909  0.976449        1.077217\n",
-      "4  0.108439  0.707618        0.715878\n",
+      "0  0.556184  0.413430        0.693012\n",
+      "1  0.287546  0.091868        0.301865\n",
+      "2  0.544135  0.827519        0.990389\n",
+      "3  0.189693  0.775072        0.797947\n",
+      "4  0.961586  0.752678        1.221135\n",
       "\n",
       "HEAD df_w_cupy:\n",
       "          x         y  distance_cupy\n",
-      "0  0.729991  0.418487       0.841438\n",
-      "1  0.887041  0.875456       1.246301\n",
-      "2  0.763563  0.220629       0.794799\n",
-      "3  0.454909  0.976449       1.077217\n",
-      "4  0.108439  0.707618       0.715878\n",
+      "0  0.556184  0.413430       0.693012\n",
+      "1  0.287546  0.091868       0.301865\n",
+      "2  0.544135  0.827519       0.990389\n",
+      "3  0.189693  0.775072       0.797947\n",
+      "4  0.961586  0.752678       1.221135\n",
       "\n",
       "Max Difference cudf to numba: 2.220446049250313e-16\n",
       "Max Difference cudf to cupy: 2.220446049250313e-16\n"
@@ -1635,12 +1628,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "ab7d2484abe64998bcb17e69802f9a67",
+       "model_id": "480e33b5a2ab4415ad56720248f95117",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'points_task'), ('type', 'PointNode'), ('conf', {'npts': 1…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'points_task'), ('type', 'PointNode'), ('conf', {'npts'…"
       ]
      },
      "metadata": {},
@@ -1664,17 +1657,10 @@
    "execution_count": 31,
    "metadata": {},
    "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "\n"
-     ]
-    },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "242b0b34d6b245f0a1b807d290bdc436",
+       "model_id": "9de0eae43be14f339fd9bc04f51ceb55",
        "version_major": 2,
        "version_minor": 0
       },
@@ -1719,27 +1705,27 @@
      "text": [
       "HEAD df_w_cudf:\n",
       "          x         y  distance_cudf\n",
-      "0  0.729991  0.418487       0.841438\n",
-      "1  0.887041  0.875456       1.246301\n",
-      "2  0.763563  0.220629       0.794799\n",
-      "3  0.454909  0.976449       1.077217\n",
-      "4  0.108439  0.707618       0.715878\n",
+      "0  0.556184  0.413430       0.693012\n",
+      "1  0.287546  0.091868       0.301865\n",
+      "2  0.544135  0.827519       0.990389\n",
+      "3  0.189693  0.775072       0.797947\n",
+      "4  0.961586  0.752678       1.221135\n",
       "\n",
       "HEAD df_w_numba:\n",
       "          x         y  distance_numba\n",
-      "0  0.729991  0.418487        0.841438\n",
-      "1  0.887041  0.875456        1.246301\n",
-      "2  0.763563  0.220629        0.794799\n",
-      "3  0.454909  0.976449        1.077217\n",
-      "4  0.108439  0.707618        0.715878\n",
+      "0  0.556184  0.413430        0.693012\n",
+      "1  0.287546  0.091868        0.301865\n",
+      "2  0.544135  0.827519        0.990389\n",
+      "3  0.189693  0.775072        0.797947\n",
+      "4  0.961586  0.752678        1.221135\n",
       "\n",
       "HEAD df_w_cupy:\n",
       "          x         y  distance_cupy\n",
-      "0  0.729991  0.418487       0.841438\n",
-      "1  0.887041  0.875456       1.246301\n",
-      "2  0.763563  0.220629       0.794799\n",
-      "3  0.454909  0.976449       1.077217\n",
-      "4  0.108439  0.707618       0.715878\n",
+      "0  0.556184  0.413430       0.693012\n",
+      "1  0.287546  0.091868       0.301865\n",
+      "2  0.544135  0.827519       0.990389\n",
+      "3  0.189693  0.775072       0.797947\n",
+      "4  0.961586  0.752678       1.221135\n",
       "\n"
      ]
     }
@@ -1834,7 +1820,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.5"
+   "version": "3.8.8"
   }
  },
  "nbformat": 4,
diff --git a/gQuant/plugins/gquant_plugin/notebooks/06_xgboost_trade.ipynb b/gQuant/plugins/gquant_plugin/notebooks/06_xgboost_trade.ipynb
index a0a038c7..3153d7e0 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/06_xgboost_trade.ipynb
+++ b/gQuant/plugins/gquant_plugin/notebooks/06_xgboost_trade.ipynb
@@ -62,23 +62,23 @@
        "<td style=\"vertical-align: top; border: 0px solid white\">\n",
        "<h3 style=\"text-align: left;\">Client</h3>\n",
        "<ul style=\"text-align: left; list-style: none; margin: 0; padding: 0;\">\n",
-       "  <li><b>Scheduler: </b>tcp://127.0.0.1:34247</li>\n",
+       "  <li><b>Scheduler: </b>tcp://127.0.0.1:40455</li>\n",
        "  <li><b>Dashboard: </b><a href='http://127.0.0.1:8787/status' target='_blank'>http://127.0.0.1:8787/status</a></li>\n",
        "</ul>\n",
        "</td>\n",
        "<td style=\"vertical-align: top; border: 0px solid white\">\n",
        "<h3 style=\"text-align: left;\">Cluster</h3>\n",
        "<ul style=\"text-align: left; list-style:none; margin: 0; padding: 0;\">\n",
-       "  <li><b>Workers: </b>2</li>\n",
-       "  <li><b>Cores: </b>2</li>\n",
-       "  <li><b>Memory: </b>100.00 GB</li>\n",
+       "  <li><b>Workers: </b>4</li>\n",
+       "  <li><b>Cores: </b>4</li>\n",
+       "  <li><b>Memory: </b>270.39 GB</li>\n",
        "</ul>\n",
        "</td>\n",
        "</tr>\n",
        "</table>"
       ],
       "text/plain": [
-       "<Client: 'tcp://127.0.0.1:34247' processes=2 threads=2, memory=100.00 GB>"
+       "<Client: 'tcp://127.0.0.1:40455' processes=4 threads=4, memory=270.39 GB>"
       ]
      },
      "execution_count": 2,
@@ -115,12 +115,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "970f1e4cbc3043bb990e0dcefaa6a01f",
+       "model_id": "e3e732daf11c435daa8827f10d9274c7",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -154,14 +154,14 @@
     {
      "data": {
       "text/plain": [
-       "['/home/yi/Projects/demo_greenflow_install/notebooks/many-small/0.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/notebooks/many-small/1.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/notebooks/many-small/2.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/notebooks/many-small/3.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/notebooks/many-small/4.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/notebooks/many-small/5.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/notebooks/many-small/6.csv',\n",
-       " '/home/yi/Projects/demo_greenflow_install/notebooks/many-small/7.csv']"
+       "['/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/0.csv',\n",
+       " '/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/1.csv',\n",
+       " '/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/2.csv',\n",
+       " '/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/3.csv',\n",
+       " '/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/4.csv',\n",
+       " '/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/5.csv',\n",
+       " '/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/6.csv',\n",
+       " '/home/yidong/Projects/gQuant/gQuant/plugins/gquant_plugin/notebooks/many-small/7.csv']"
       ]
      },
      "execution_count": 4,
@@ -174,8 +174,7 @@
     "import os\n",
     "num_partitions = 8\n",
     "\n",
-    "os.makedirs('many-small', exist_ok=True)\n",
-    "dd.from_pandas(input_cached.set_index('asset'), npartitions=num_partitions).reset_index().to_csv('many-small/*.csv', index=False)"
+    "dd.from_pandas(input_cached, npartitions=num_partitions).shuffle(on=['asset']).to_csv('many-small/*.csv', index=False)"
    ]
   },
   {
@@ -187,14 +186,14 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 5,
    "metadata": {},
    "outputs": [
     {
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "0.17.0\n"
+      "0.18.0\n"
      ]
     }
    ],
@@ -241,12 +240,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "6eb535c0bcee4a3d8b92cdd41c991967",
+       "model_id": "9c75dbfdd7de41d4a8c08a695bbd948c",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -273,12 +272,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "49e4668f4fdf4a5cb7656b4fe42b7fd0",
+       "model_id": "55c74df1b3bd4f90925906943425164e",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -313,237 +312,68 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "class XGBoostStrategyNode(_PortTypesMixin, Node):\n",
+      "class XGBoostStrategyNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):\n",
       "    \"\"\"\n",
       "    This is the Node used to compute trading signal from XGBoost Strategy.\n",
-      "    It requires the following conf fields:\n",
-      "        \"train_date\": a date string of \"Y-m-d\" format. All the data points\n",
-      "        before this date is considered as training, otherwise as testing. If\n",
-      "        not provided, all the data points are considered as training.\n",
-      "        \"xgboost_parameters\": a dictionary of any legal parameters for XGBoost\n",
-      "        models. It overwrites the default parameters used in the process method\n",
-      "        \"no_feature\": specifying a list of columns in the input dataframe that\n",
-      "        should NOT be considered as training features.\n",
-      "        \"target\": the column that is considered as \"target\" in machine learning\n",
-      "        algorithm\n",
-      "    It requires the \"datetime\" column for spliting the data points and adds a\n",
-      "    new column \"signal\" to be used for backtesting.\n",
-      "    The detailed computation steps are listed in the process method's docstring\n",
+      "\n",
       "    \"\"\"\n",
       "\n",
       "    def init(self):\n",
-      "        _PortTypesMixin.init(self)\n",
+      "        TemplateNodeMixin.init(self)\n",
       "        self.INPUT_PORT_NAME = 'stock_in'\n",
       "        self.OUTPUT_PORT_NAME = 'stock_out'\n",
-      "\n",
-      "    def meta_setup(self):\n",
-      "        # if 'no_feature' in self.conf:\n",
-      "        #     retention = self.conf['no_feature']\n",
-      "        # else:\n",
-      "        cols_required = {'datetime': 'date',\n",
-      "                         \"asset\": \"int64\"}\n",
-      "        # self.delayed_process = True\n",
-      "        required = {\n",
+      "        port_type = PortsSpecSchema.port_type\n",
+      "        self.port_inports = {\n",
+      "            self.INPUT_PORT_NAME: {\n",
+      "                port_type: [\n",
+      "                    \"pandas.DataFrame\", \"cudf.DataFrame\",\n",
+      "                    \"dask_cudf.DataFrame\", \"dask.dataframe.DataFrame\"\n",
+      "                ]\n",
+      "            },\n",
+      "        }\n",
+      "        self.port_outports = {\n",
+      "            self.OUTPUT_PORT_NAME: {\n",
+      "                port_type: \"${port:stock_in}\"\n",
+      "            }\n",
+      "        }\n",
+      "        cols_required = {'predict': None, \"asset\": \"int64\"}\n",
+      "        addition = {}\n",
+      "        addition['signal'] = 'float64'\n",
+      "        self.meta_inports = {\n",
       "            self.INPUT_PORT_NAME: cols_required\n",
       "        }\n",
-      "        retention = {}\n",
-      "        retention['signal'] = 'float64'\n",
-      "        # _PortTypesMixin.retention_meta_setup(self, retention)\n",
-      "\n",
-      "        input_meta = self.get_input_meta()\n",
-      "        if self.INPUT_PORT_NAME not in input_meta:\n",
-      "            col_from_inport = required[self.INPUT_PORT_NAME]\n",
-      "        else:\n",
-      "            col_from_inport = input_meta[self.INPUT_PORT_NAME]\n",
-      "        # delete the columns from the inputs\n",
-      "        if 'no_feature' in self.conf:\n",
-      "            for key in self.conf['no_feature']:\n",
-      "                if key in col_from_inport:\n",
-      "                    retention[key] = col_from_inport[key]\n",
-      "        metadata = MetaData(inports=required,\n",
-      "                            outports={self.OUTPUT_PORT_NAME: retention})\n",
-      "        return metadata\n",
-      "\n",
-      "    def ports_setup(self):\n",
-      "        types = [cudf.DataFrame,\n",
-      "                 dask_cudf.DataFrame]\n",
-      "        return _PortTypesMixin.ports_setup_from_types(self, types)\n",
+      "        self.meta_outports = {\n",
+      "            self.OUTPUT_PORT_NAME: {\n",
+      "                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,\n",
+      "                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,\n",
+      "                MetaDataSchema.META_DATA: addition\n",
+      "            }\n",
+      "        }\n",
       "\n",
       "    def conf_schema(self):\n",
       "        json = {\n",
       "            \"title\": \"XGBoost Node configure\",\n",
       "            \"type\": \"object\",\n",
-      "            \"description\": \"\"\"Split the data into training and testing based on\n",
-      "             'train_data', train a XGBoost model based on the training data,\n",
-      "             make predictions for all the data points, compute the trading.\n",
+      "            \"description\": \"\"\"convert the predicted next day return as trading actions\n",
       "            \"\"\",\n",
       "            \"properties\": {\n",
-      "                \"num_of_rounds\": {\n",
-      "                    \"type\": \"number\",\n",
-      "                    \"description\": \"\"\"The number of rounds for boosting\"\"\",\n",
-      "                    \"default\": 100\n",
-      "                },\n",
-      "                \"train_date\":  {\n",
-      "                    \"type\": \"string\",\n",
-      "                    \"description\": \"\"\"the date to splite train and validation\n",
-      "                    dataset\"\"\"\n",
-      "                },\n",
-      "                \"target\":  {\n",
-      "                    \"type\": \"string\",\n",
-      "                    \"description\": \"the column used as dependent variable\"\n",
-      "                },\n",
-      "                \"no_feature\": {\n",
-      "                    \"type\": \"array\",\n",
-      "                    \"items\": {\n",
-      "                        \"type\": \"string\",\n",
-      "                    },\n",
-      "                    \"description\": \"\"\"columns in the input dataframe that\n",
-      "        should NOT be considered as training features.\"\"\"\n",
-      "                },\n",
-      "                \"xgboost_parameters\": {\n",
-      "                    \"type\": \"object\",\n",
-      "                    \"description\": \"xgoobst parameters\",\n",
-      "                    \"properties\": {\n",
-      "                        'max_depth': {\n",
-      "                            \"type\": \"number\",\n",
-      "                            \"description\": \"Maximum depth of a tree.\",\n",
-      "                            \"default\": 8\n",
-      "                        },\n",
-      "                        \"max_leaves\": {\n",
-      "                            \"type\": \"number\",\n",
-      "                            \"description\": \"maximum number of tree leaves\",\n",
-      "                            \"default\": 2**8\n",
-      "                        },\n",
-      "                        \"gamma\": {\n",
-      "                            \"type\": \"number\",\n",
-      "                            \"description\": \"\"\"Minimum loss reduction required\n",
-      "                            to make a further partition on a leaf node of the\n",
-      "                            tree.\"\"\",\n",
-      "                            \"default\": 0\n",
-      "                        },\n",
-      "                        \"objective\": {\n",
-      "                            \"type\": \"string\",\n",
-      "                            \"enum\": [\"reg:squarederror\", \"reg:squaredlogerror\",\n",
-      "                                     \"reg:logistic\", \"reg:pseudohubererror\"],\n",
-      "                            \"description\": \"\"\"Specify the learning task and\n",
-      "                            the corresponding learning objective.\"\"\",\n",
-      "                            \"default\": \"reg:squarederror\"\n",
-      "                        }\n",
-      "                    }\n",
-      "                }\n",
       "            },\n",
-      "            \"required\": [\"target\", \"num_of_rounds\"],\n",
       "        }\n",
       "        ui = {\n",
-      "            \"train_date\":  {\n",
-      "                \"ui:widget\": \"alt-date\",\n",
-      "                \"ui:options\": {\n",
-      "                        \"yearsRange\": [1985, 2025],\n",
-      "                        \"hideNowButton\": True,\n",
-      "                        \"hideClearButton\": True,\n",
-      "                },\n",
-      "            },\n",
       "        }\n",
-      "        input_meta = self.get_input_meta()\n",
-      "        if self.INPUT_PORT_NAME in input_meta:\n",
-      "            col_from_inport = input_meta[self.INPUT_PORT_NAME]\n",
-      "            enums = [col for col in col_from_inport.keys()]\n",
-      "            json['properties']['no_feature']['items']['enum'] = enums\n",
-      "            json['properties']['target']['enum'] = enums\n",
-      "            return ConfSchema(json=json, ui=ui)\n",
-      "        else:\n",
-      "            return ConfSchema(json=json, ui=ui)\n",
+      "        return ConfSchema(json=json, ui=ui)\n",
       "\n",
       "    def process(self, inputs):\n",
-      "        \"\"\"\n",
-      "        The process is doing following things:\n",
-      "            1. split the data into training and testing based on provided\n",
-      "               conf['train_date']. If it is not provided, all the data is\n",
-      "               treated as training data.\n",
-      "            2. train a XGBoost model based on the training data\n",
-      "            3. Make predictions for all the data points including training and\n",
-      "               testing.\n",
-      "            4. From the prediction of returns, compute the trading signals that\n",
-      "               can be used in the backtesting.\n",
-      "        Arguments\n",
-      "        -------\n",
-      "         inputs: list\n",
-      "            list of input dataframes.\n",
-      "        Returns\n",
-      "        -------\n",
-      "        dataframe\n",
-      "        \"\"\"\n",
-      "        dxgb_params = {\n",
-      "                'max_depth':         8,\n",
-      "                'max_leaves':        2 ** 8,\n",
-      "                'tree_method':       'gpu_hist',\n",
-      "                'objective':         'reg:squarederror',\n",
-      "                'grow_policy':       'lossguide',\n",
-      "        }\n",
-      "        # num_of_rounds = 100\n",
-      "        if 'xgboost_parameters' in self.conf:\n",
-      "            dxgb_params.update(self.conf['xgboost_parameters'])\n",
       "        input_df = inputs[self.INPUT_PORT_NAME]\n",
-      "        model_df = input_df\n",
-      "        train_cols = set(model_df.columns) - set(\n",
-      "            self.conf['no_feature'])\n",
-      "        train_cols = list(train_cols - set([self.conf['target']]))\n",
-      "\n",
-      "        if isinstance(input_df, dask_cudf.DataFrame):\n",
-      "            # get the client\n",
-      "            client = dask.distributed.client.default_client()\n",
-      "            if 'train_date' in self.conf:\n",
-      "                train_date = datetime.datetime.strptime(self.conf['train_date'],  # noqa: F841, E501\n",
-      "                                                        '%Y-%m-%d')\n",
-      "                model_df = model_df[model_df.datetime < train_date]\n",
-      "            train = model_df[train_cols]\n",
-      "            target = model_df[self.conf['target']]\n",
-      "            dmatrix = xgb.dask.DaskDMatrix(client, train, label=target)\n",
-      "            bst = xgb.dask.train(client, dxgb_params, dmatrix,\n",
-      "                                 num_boost_round=self.conf[\"num_of_rounds\"])\n",
-      "\n",
-      "            dtrain = xgb.dask.DaskDMatrix(client, input_df[train_cols])\n",
-      "            prediction = xgb.dask.predict(client, bst, dtrain).persist()\n",
-      "            pred_df = dask_cudf.from_dask_dataframe(\n",
-      "                prediction.to_dask_dataframe())\n",
-      "            pred_df.index = input_df.index\n",
-      "            input_df['signal'] = pred_df\n",
-      "        elif isinstance(input_df, cudf.DataFrame):\n",
-      "            if 'train_date' in self.conf:\n",
-      "                train_date = datetime.datetime.strptime(self.conf['train_date'],  # noqa: F841, E501\n",
-      "                                                        '%Y-%m-%d')\n",
-      "                model_df = model_df.query('datetime<@train_date')\n",
-      "            train = model_df[train_cols]\n",
-      "            target = model_df[self.conf['target']]\n",
-      "            dmatrix = xgb.DMatrix(train, label=target)\n",
-      "            bst = xgb.train(dxgb_params, dmatrix,\n",
-      "                            num_boost_round=self.conf[\"num_of_rounds\"])\n",
-      "            infer_dmatrix = xgb.DMatrix(input_df[train_cols])\n",
-      "            prediction = cudf.Series(bst.predict(infer_dmatrix),\n",
-      "                                     nan_as_null=False,\n",
-      "                                     index=input_df.index\n",
-      "                                     ).astype('float64')\n",
-      "            input_df['signal'] = prediction\n",
-      "\n",
-      "        input_df['tmp'] = (input_df['asset'] -\n",
-      "                           input_df['asset'].shift(1)).fillna(1)\n",
-      "        input_df['tmp'] = (input_df['tmp'] != 0).astype('int32')\n",
-      "        tmp = input_df['tmp']\n",
-      "        input_df['tmp'] = tmp.where(tmp != 1, None)\n",
-      "        input_df = input_df.dropna(subset=['tmp'])\n",
-      "        input_df = input_df.drop('tmp', axis=1)\n",
-      "\n",
       "        # convert the signal to trading action\n",
       "        # 1 is buy and -1 is sell\n",
       "        # It predicts the tomorrow's return (shift -1)\n",
       "        # We shift 1 for trading actions so that it acts on the second day\n",
       "        input_df['signal'] = ((\n",
-      "            input_df['signal'] >= 0).astype('float') * 2 - 1).shift(1)\n",
-      "\n",
+      "            input_df['predict'] >= 0).astype('float') * 2 - 1).shift(1)\n",
       "        # remove the bad datapints\n",
       "        input_df = input_df.dropna()\n",
-      "        remaining = list(self.conf['no_feature']) + ['signal']\n",
-      "        return {self.OUTPUT_PORT_NAME: input_df[remaining]}\n",
+      "        return {self.OUTPUT_PORT_NAME: input_df}\n",
       "\n"
      ]
     }
@@ -573,7 +403,7 @@
      "output_type": "stream",
      "text": [
       "output meta of node node_technical_indicator:\n",
-      "MetaData(inports={'stock_in': {'indicator': 'int32', 'high': 'float64', 'low': 'float64', 'close': 'float64', 'volume': 'float64', 'returns': 'float64'}}, outports={'stock_out': {'CH_OS_10_20': 'float64', 'BO_BA_b1_10': 'float64', 'BO_BA_b2_10': 'float64', 'SHIFT_-1': 'float64', 'indicator': 'int32', 'returns': 'float64', 'datetime': 'date', 'asset': 'int64', 'volume': 'float64', 'close': 'float64', 'open': 'float64', 'high': 'float64', 'low': 'float64'}})\n"
+      "MetaData(inports={'stock_in': {'indicator': 'int32', 'high': 'float64', 'low': 'float64', 'close': 'float64', 'volume': 'float64', 'returns': 'float64'}}, outports={'stock_out': {'datetime': 'datetime64[ns]', 'open': 'float64', 'close': 'float64', 'high': 'float64', 'low': 'float64', 'asset': 'int64', 'volume': 'float64', 'returns': 'float64', 'indicator': 'int32', 'CH_OS_10_20': 'float64', 'BO_BA_b1_10': 'float64', 'BO_BA_b2_10': 'float64', 'SHIFT_-1': 'float64'}})\n"
      ]
     }
    ],
@@ -603,22 +433,28 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "id:stock_data process time:4.270s\n",
-      "id:preprocess process time:0.761s\n",
-      "id:sort_after process time:0.056s\n",
-      "id:technical_indicator process time:2.571s\n",
-      "id:xgboost process time:2.495s\n",
-      "id:backtest process time:0.001s\n",
-      "id:train_df process time:0.125s\n",
-      "id:portfolio_opt_train process time:0.012s\n",
+      "id:stock_data process time:3.503s\n",
+      "id:preprocess process time:0.762s\n",
+      "id:sort_after process time:0.072s\n",
+      "id:technical_indicator process time:2.605s\n",
+      "id:train_df process time:0.134s\n",
+      "id:validation_df process time:0.012s\n",
+      "id:persist process time:0.000s\n",
+      "id:xboost_model process time:1.745s\n",
+      "id:train_infer process time:0.084s\n",
+      "id:train_signal process time:0.024s\n",
+      "id:train_backtest process time:0.001s\n",
+      "id:portfolio_opt_train process time:0.011s\n",
       "id:sharpe_ratio_trn process time:0.001s\n",
-      "id:cumulative_return_trn process time:0.016s\n",
-      "id:validation_df process time:0.006s\n",
-      "id:portfolio_opt_validation process time:0.009s\n",
+      "id:cumulative_return_trn process time:0.021s\n",
+      "id:val_infer process time:0.051s\n",
+      "id:val_signal process time:0.023s\n",
+      "id:val_backtest process time:0.001s\n",
+      "id:portfolio_opt_validation process time:0.012s\n",
       "id:sharpe_ratio_val process time:0.001s\n",
-      "id:cumulative_return_val process time:0.015s\n",
-      "CPU times: user 10 s, sys: 1.43 s, total: 11.4 s\n",
-      "Wall time: 11.1 s\n"
+      "id:cumulative_return_val process time:0.020s\n",
+      "CPU times: user 8.22 s, sys: 1.26 s, total: 9.48 s\n",
+      "Wall time: 9.28 s\n"
      ]
     }
    ],
@@ -643,17 +479,19 @@
   {
    "cell_type": "code",
    "execution_count": 11,
-   "metadata": {},
+   "metadata": {
+    "tags": []
+   },
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "99ac1d90b02b407b9a9df08658f437b8",
+       "model_id": "697526d60c7d4c16bef69dca493a8049",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "VBox(children=(Figure(axes=[Axis(label='Cumulative return', orientation='vertical', scale=LinearScale(), side=…"
+       "VBox(children=(Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x02\\xd0\\x00\\x00\\x01 \\x08\\x06\\x00\\x00\\…"
       ]
      },
      "metadata": {},
@@ -661,23 +499,40 @@
     }
    ],
    "source": [
+    "def fig2img(fig):\n",
+    "    \"\"\"Convert a Matplotlib figure to BufferIO\"\"\"\n",
+    "    import io\n",
+    "    buf = io.BytesIO()\n",
+    "    fig.savefig(buf)\n",
+    "    buf.seek(0)\n",
+    "    return buf\n",
+    "\n",
     "# define the function to format the plots\n",
     "def plot_figures(o):\n",
-    "    # format the figures\n",
-    "    figure_width = '1200px'\n",
-    "    figure_height = '400px'\n",
+    "\n",
     "    sharpe_number = o['sharpe_ratio_trn.sharpe_out']\n",
     "    cum_return_train = o['cumulative_return_trn.cum_return']\n",
-    "    cum_return_train.layout.height = figure_height\n",
-    "    cum_return_train.layout.width = figure_width\n",
-    "    cum_return_train.title = 'Training P & L %.3f' % (sharpe_number)\n",
+    "    cum_return_train.set_figwidth(10)\n",
+    "    cum_return_train.suptitle('Train P & L %.3f' % (sharpe_number), fontsize=16)\n",
+    "    i = fig2img(cum_return_train)\n",
+    "    img_train = widgets.Image(\n",
+    "                        value=i.read(),\n",
+    "                        format='png',\n",
+    "                        width=600,\n",
+    "                        height=900,\n",
+    "    )\n",
     "    sharpe_number = o['sharpe_ratio_val.sharpe_out']\n",
     "    cum_return_test = o['cumulative_return_val.cum_return']\n",
-    "    cum_return_test.layout.height = figure_height\n",
-    "    cum_return_test.layout.width = figure_width\n",
-    "    cum_return_test.title = 'Testing P & L %.3f' % (sharpe_number)\n",
-    "\n",
-    "    return widgets.VBox([cum_return_train, cum_return_test])\n",
+    "    cum_return_test.set_figwidth(10)\n",
+    "    cum_return_test.suptitle('Test P & L %.3f' % (sharpe_number), fontsize=16)\n",
+    "    i = fig2img(cum_return_test)\n",
+    "    img_test = widgets.Image(\n",
+    "                        value=i.read(),\n",
+    "                        format='png',\n",
+    "                        width=600,\n",
+    "                        height=900,\n",
+    "    )\n",
+    "    return widgets.VBox([img_train, img_test])\n",
     "plot_figures(o_gpu)"
    ]
   },
@@ -708,20 +563,26 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "id:stock_data process time:0.010s\n",
-      "id:preprocess process time:7.655s\n",
-      "id:xgboost process time:3.325s\n",
-      "id:backtest process time:0.010s\n",
-      "id:train_df process time:0.143s\n",
-      "id:portfolio_opt_train process time:0.033s\n",
-      "id:sharpe_ratio_trn process time:1.407s\n",
-      "id:cumulative_return_trn process time:1.193s\n",
+      "id:stock_data process time:0.009s\n",
+      "id:preprocess process time:0.361s\n",
+      "id:train_df process time:0.007s\n",
       "id:validation_df process time:0.007s\n",
-      "id:portfolio_opt_validation process time:0.032s\n",
-      "id:sharpe_ratio_val process time:1.172s\n",
-      "id:cumulative_return_val process time:1.268s\n",
-      "CPU times: user 8.24 s, sys: 801 ms, total: 9.04 s\n",
-      "Wall time: 29.7 s\n"
+      "id:persist process time:0.138s\n",
+      "id:xboost_model process time:9.820s\n",
+      "id:train_infer process time:0.675s\n",
+      "id:train_signal process time:0.026s\n",
+      "id:train_backtest process time:0.014s\n",
+      "id:portfolio_opt_train process time:0.022s\n",
+      "id:sharpe_ratio_trn process time:0.368s\n",
+      "id:cumulative_return_trn process time:0.374s\n",
+      "id:val_infer process time:0.639s\n",
+      "id:val_signal process time:0.028s\n",
+      "id:val_backtest process time:0.012s\n",
+      "id:portfolio_opt_validation process time:0.023s\n",
+      "id:sharpe_ratio_val process time:0.316s\n",
+      "id:cumulative_return_val process time:0.330s\n",
+      "CPU times: user 3.62 s, sys: 266 ms, total: 3.88 s\n",
+      "Wall time: 13.5 s\n"
      ]
     }
    ],
@@ -740,12 +601,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "98b03eaa90714b2381a5563914964296",
+       "model_id": "60bb24ffb75d4fa48288be56b4be0c4c",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "VBox(children=(Figure(axes=[Axis(label='Cumulative return', orientation='vertical', scale=LinearScale(), side=…"
+       "VBox(children=(Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x02\\xd0\\x00\\x00\\x01 \\x08\\x06\\x00\\x00\\…"
       ]
      },
      "metadata": {},
@@ -898,12 +759,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "49e4668f4fdf4a5cb7656b4fe42b7fd0",
+       "model_id": "55c74df1b3bd4f90925906943425164e",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(cache={'nodes': [{'width': 140, 'id': 'stock_data', 'type': 'CsvStockLoader', 'schema': {'title':…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -930,19 +791,25 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "id:technical_indicator process time:2.659s\n",
-      "id:xgboost process time:5.527s\n",
-      "id:backtest process time:0.002s\n",
-      "id:train_df process time:0.007s\n",
+      "id:technical_indicator process time:3.018s\n",
+      "id:train_df process time:0.055s\n",
+      "id:validation_df process time:0.024s\n",
+      "id:persist process time:0.000s\n",
+      "id:xboost_model process time:3.674s\n",
+      "id:train_infer process time:0.109s\n",
+      "id:train_signal process time:0.059s\n",
+      "id:train_backtest process time:0.001s\n",
       "id:portfolio_opt_train process time:0.011s\n",
       "id:sharpe_ratio_trn process time:0.001s\n",
-      "id:cumulative_return_trn process time:0.015s\n",
-      "id:validation_df process time:0.005s\n",
-      "id:portfolio_opt_validation process time:0.009s\n",
+      "id:cumulative_return_trn process time:0.021s\n",
+      "id:val_infer process time:0.078s\n",
+      "id:val_signal process time:0.035s\n",
+      "id:val_backtest process time:0.001s\n",
+      "id:portfolio_opt_validation process time:0.010s\n",
       "id:sharpe_ratio_val process time:0.001s\n",
-      "id:cumulative_return_val process time:0.015s\n",
-      "CPU times: user 7.86 s, sys: 1.37 s, total: 9.22 s\n",
-      "Wall time: 8.89 s\n"
+      "id:cumulative_return_val process time:0.020s\n",
+      "CPU times: user 6.11 s, sys: 1.43 s, total: 7.54 s\n",
+      "Wall time: 7.45 s\n"
      ]
     }
    ],
@@ -964,12 +831,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "c37ca8d7dc8a487fbe99d187748ecb48",
+       "model_id": "a4140131d63f4b3e9e23f2cee291437d",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "VBox(children=(Figure(axes=[Axis(label='Cumulative return', orientation='vertical', scale=LinearScale(), side=…"
+       "VBox(children=(Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x02\\xd0\\x00\\x00\\x01 \\x08\\x06\\x00\\x00\\…"
       ]
      },
      "metadata": {},
@@ -999,30 +866,36 @@
      "output_type": "stream",
      "text": [
       "id:stock_data process time:0.007s\n",
-      "id:preprocess process time:5.938s\n",
-      "id:xgboost process time:9.109s\n",
-      "id:backtest process time:0.010s\n",
-      "id:train_df process time:0.007s\n",
-      "id:portfolio_opt_train process time:0.032s\n",
-      "id:sharpe_ratio_trn process time:2.444s\n",
-      "id:cumulative_return_trn process time:2.464s\n",
-      "id:validation_df process time:0.006s\n",
-      "id:portfolio_opt_validation process time:0.032s\n",
-      "id:sharpe_ratio_val process time:2.344s\n",
-      "id:cumulative_return_val process time:2.389s\n",
-      "CPU times: user 10 s, sys: 2.84 s, total: 12.9 s\n",
-      "Wall time: 43.4 s\n"
+      "id:preprocess process time:0.360s\n",
+      "id:train_df process time:0.009s\n",
+      "id:validation_df process time:0.009s\n",
+      "id:persist process time:0.135s\n",
+      "id:xboost_model process time:11.210s\n",
+      "id:train_infer process time:0.619s\n",
+      "id:train_signal process time:0.031s\n",
+      "id:train_backtest process time:0.014s\n",
+      "id:portfolio_opt_train process time:0.021s\n",
+      "id:sharpe_ratio_trn process time:0.497s\n",
+      "id:cumulative_return_trn process time:0.567s\n",
+      "id:val_infer process time:0.674s\n",
+      "id:val_signal process time:0.035s\n",
+      "id:val_backtest process time:0.016s\n",
+      "id:portfolio_opt_validation process time:0.024s\n",
+      "id:sharpe_ratio_val process time:0.445s\n",
+      "id:cumulative_return_val process time:0.450s\n",
+      "CPU times: user 4.12 s, sys: 338 ms, total: 4.45 s\n",
+      "Wall time: 15.9 s\n"
      ]
     },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "4f37ade2840b4a6db394f4c07f5bca47",
+       "model_id": "dc30cf1090ad4ddaa62a5877fee6b75b",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "VBox(children=(Figure(axes=[Axis(label='Cumulative return', orientation='vertical', scale=LinearScale()), Axis…"
+       "VBox(children=(Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x02\\xd0\\x00\\x00\\x01 \\x08\\x06\\x00\\x00\\…"
       ]
      },
      "metadata": {},
@@ -1070,12 +943,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "dd477080878b49c28174a3c3b1218369",
+       "model_id": "62a9778490d844b195f42c07b661160b",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "VBox(children=(Figure(axes=[Axis(label='Cumulative return', orientation='vertical', scale=LinearScale()), Axis…"
+       "VBox(children=(Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x02\\xd0\\x00\\x00\\x01 \\x08\\x06\\x00\\x00\\…"
       ]
      },
      "metadata": {},
@@ -1111,7 +984,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "4907f1afde0445f6bea3c99150c57d5a",
+       "model_id": "c072907f95fe42779d8f9ff81d651076",
        "version_major": 2,
        "version_minor": 0
       },
@@ -1185,7 +1058,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.5"
+   "version": "3.8.8"
   }
  },
  "nbformat": 4,
diff --git a/gQuant/plugins/gquant_plugin/notebooks/07_fractional_differencing.ipynb b/gQuant/plugins/gquant_plugin/notebooks/07_fractional_differencing.ipynb
index 473b4f63..9ea3ca8c 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/07_fractional_differencing.ipynb
+++ b/gQuant/plugins/gquant_plugin/notebooks/07_fractional_differencing.ipynb
@@ -34,7 +34,6 @@
     "import inspect\n",
     "from numba import njit\n",
     "from numba import prange\n",
-    "from greenflow.dataframe_flow.task import load_modules\n",
     "from greenflow_gquant_plugin.cuindicator import get_weights_floored, fractional_diff\n",
     "warnings.simplefilter(\"ignore\")"
    ]
@@ -416,10 +415,10 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "array size 100000, Ensemble: time 0.273 s, greenflow GPU Time 0.374 s, greenflow CPU Time 0.499, speed up 0.73, speed up vs CPU 1.33, error 0.0000 \n",
-      "array size 1000000, Ensemble: time 0.144 s, greenflow GPU Time 0.007 s, greenflow CPU Time 0.035, speed up 20.04, speed up vs CPU 4.83, error 0.0000 \n",
-      "array size 10000000, Ensemble: time 1.023 s, greenflow GPU Time 0.031 s, greenflow CPU Time 0.125, speed up 32.87, speed up vs CPU 4.02, error 0.0000 \n",
-      "array size 100000000, Ensemble: time 10.207 s, greenflow GPU Time 0.267 s, greenflow CPU Time 1.057, speed up 38.18, speed up vs CPU 3.95, error 0.0000 \n"
+      "array size 100000, Ensemble: time 0.243 s, greenflow GPU Time 0.353 s, greenflow CPU Time 0.435, speed up 0.69, speed up vs CPU 1.23, error 0.0000 \n",
+      "array size 1000000, Ensemble: time 0.140 s, greenflow GPU Time 0.005 s, greenflow CPU Time 0.044, speed up 30.35, speed up vs CPU 9.58, error 0.0000 \n",
+      "array size 10000000, Ensemble: time 1.142 s, greenflow GPU Time 0.014 s, greenflow CPU Time 0.335, speed up 84.55, speed up vs CPU 24.82, error 0.0000 \n",
+      "array size 100000000, Ensemble: time 11.127 s, greenflow GPU Time 0.099 s, greenflow CPU Time 3.084, speed up 111.91, speed up vs CPU 31.02, error 0.0000 \n"
      ]
     }
    ],
@@ -501,12 +500,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "d1a3e7ad5d224709a8e092c3e4140b36",
+       "model_id": "2ce8efe02cfc41a9bf034a3dd4738414",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -541,22 +540,40 @@
    "metadata": {},
    "outputs": [],
    "source": [
+    "def fig2img(fig):\n",
+    "    \"\"\"Convert a Matplotlib figure to BufferIO\"\"\"\n",
+    "    import io\n",
+    "    buf = io.BytesIO()\n",
+    "    fig.savefig(buf)\n",
+    "    buf.seek(0)\n",
+    "    return buf\n",
+    "\n",
     "# define the function to format the plots\n",
     "def plot_figures(o):\n",
-    "    # format the figures\n",
-    "    figure_width = '1200px'\n",
-    "    figure_height = '400px'\n",
+    "\n",
     "    sharpe_number = o['sharpe_ratio_trn.sharpe_out']\n",
     "    cum_return_train = o['cumulative_return_trn.cum_return']\n",
-    "    cum_return_train.layout.height = figure_height\n",
-    "    cum_return_train.layout.width = figure_width\n",
-    "    cum_return_train.title = 'Training P & L %.3f' % (sharpe_number)\n",
+    "    cum_return_train.set_figwidth(10)\n",
+    "    cum_return_train.suptitle('Train P & L %.3f' % (sharpe_number), fontsize=16)\n",
+    "    i = fig2img(cum_return_train)\n",
+    "    img_train = widgets.Image(\n",
+    "                        value=i.read(),\n",
+    "                        format='png',\n",
+    "                        width=600,\n",
+    "                        height=900,\n",
+    "    )\n",
     "    sharpe_number = o['sharpe_ratio_val.sharpe_out']\n",
     "    cum_return_test = o['cumulative_return_val.cum_return']\n",
-    "    cum_return_test.layout.height = figure_height\n",
-    "    cum_return_test.layout.width = figure_width\n",
-    "    cum_return_test.title = 'Testing P & L %.3f' % (sharpe_number)\n",
-    "    return widgets.VBox([cum_return_train, cum_return_test])"
+    "    cum_return_test.set_figwidth(10)\n",
+    "    cum_return_test.suptitle('Test P & L %.3f' % (sharpe_number), fontsize=16)\n",
+    "    i = fig2img(cum_return_test)\n",
+    "    img_test = widgets.Image(\n",
+    "                        value=i.read(),\n",
+    "                        format='png',\n",
+    "                        width=600,\n",
+    "                        height=900,\n",
+    "    )\n",
+    "    return widgets.VBox([img_train, img_test])\n"
    ]
   },
   {
@@ -619,31 +636,37 @@
      "name": "stdout",
      "output_type": "stream",
      "text": [
-      "id:stock_data process time:4.146s\n",
-      "id:preprocess process time:0.752s\n",
-      "id:sort_after process time:0.056s\n",
-      "id:technical_indicator process time:0.459s\n",
-      "id:xgboost process time:2.345s\n",
-      "id:backtest process time:0.001s\n",
-      "id:train_df process time:0.144s\n",
-      "id:portfolio_opt_train process time:0.011s\n",
+      "id:stock_data process time:3.222s\n",
+      "id:preprocess process time:0.778s\n",
+      "id:sort_after process time:0.074s\n",
+      "id:technical_indicator process time:0.470s\n",
+      "id:train_df process time:0.125s\n",
+      "id:validation_df process time:0.012s\n",
+      "id:persist process time:0.000s\n",
+      "id:xboost_model process time:1.747s\n",
+      "id:train_infer process time:0.094s\n",
+      "id:train_signal process time:0.024s\n",
+      "id:train_backtest process time:0.001s\n",
+      "id:portfolio_opt_train process time:0.012s\n",
       "id:sharpe_ratio_trn process time:0.001s\n",
-      "id:cumulative_return_trn process time:0.015s\n",
-      "id:validation_df process time:0.005s\n",
-      "id:portfolio_opt_validation process time:0.009s\n",
+      "id:cumulative_return_trn process time:0.022s\n",
+      "id:val_infer process time:0.049s\n",
+      "id:val_signal process time:0.018s\n",
+      "id:val_backtest process time:0.001s\n",
+      "id:portfolio_opt_validation process time:0.010s\n",
       "id:sharpe_ratio_val process time:0.001s\n",
-      "id:cumulative_return_val process time:0.014s\n"
+      "id:cumulative_return_val process time:0.019s\n"
      ]
     },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "aabd384e30614c609ee3f881b974d53c",
+       "model_id": "80c08629428241b3ab6b65db1c15fbe6",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "VBox(children=(Figure(axes=[Axis(label='Cumulative return', orientation='vertical', scale=LinearScale()), Axis…"
+       "VBox(children=(Image(value=b'\\x89PNG\\r\\n\\x1a\\n\\x00\\x00\\x00\\rIHDR\\x00\\x00\\x02\\xd0\\x00\\x00\\x01 \\x08\\x06\\x00\\x00\\…"
       ]
      },
      "metadata": {},
@@ -682,12 +705,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "a75a2bfde85b4e9f82674b7c8f98f12a",
+       "model_id": "0e393564429f4eebb4513d3650204b49",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -704,17 +727,10 @@
    "execution_count": 12,
    "metadata": {},
    "outputs": [
-    {
-     "name": "stdout",
-     "output_type": "stream",
-     "text": [
-      "\n"
-     ]
-    },
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "b3c117b3f5634731938dbdd0fe2525c1",
+       "model_id": "d2ae5d6bc58c479ab1bfc878ffd4e6a9",
        "version_major": 2,
        "version_minor": 0
       },
@@ -798,7 +814,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.5"
+   "version": "3.8.8"
   }
  },
  "nbformat": 4,
diff --git a/gQuant/plugins/gquant_plugin/notebooks/08_greenflow_machine_learning.ipynb b/gQuant/plugins/gquant_plugin/notebooks/08_gquant_machine_learning.ipynb
similarity index 92%
rename from gQuant/plugins/gquant_plugin/notebooks/08_greenflow_machine_learning.ipynb
rename to gQuant/plugins/gquant_plugin/notebooks/08_gquant_machine_learning.ipynb
index 3e793b35..79a18b5b 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/08_greenflow_machine_learning.ipynb
+++ b/gQuant/plugins/gquant_plugin/notebooks/08_gquant_machine_learning.ipynb
@@ -59,23 +59,23 @@
        "<td style=\"vertical-align: top; border: 0px solid white\">\n",
        "<h3 style=\"text-align: left;\">Client</h3>\n",
        "<ul style=\"text-align: left; list-style: none; margin: 0; padding: 0;\">\n",
-       "  <li><b>Scheduler: </b>tcp://127.0.0.1:44741</li>\n",
+       "  <li><b>Scheduler: </b>tcp://127.0.0.1:42607</li>\n",
        "  <li><b>Dashboard: </b><a href='http://127.0.0.1:8787/status' target='_blank'>http://127.0.0.1:8787/status</a></li>\n",
        "</ul>\n",
        "</td>\n",
        "<td style=\"vertical-align: top; border: 0px solid white\">\n",
        "<h3 style=\"text-align: left;\">Cluster</h3>\n",
        "<ul style=\"text-align: left; list-style:none; margin: 0; padding: 0;\">\n",
-       "  <li><b>Workers: </b>2</li>\n",
-       "  <li><b>Cores: </b>2</li>\n",
-       "  <li><b>Memory: </b>100.00 GB</li>\n",
+       "  <li><b>Workers: </b>4</li>\n",
+       "  <li><b>Cores: </b>4</li>\n",
+       "  <li><b>Memory: </b>270.39 GB</li>\n",
        "</ul>\n",
        "</td>\n",
        "</tr>\n",
        "</table>"
       ],
       "text/plain": [
-       "<Client: 'tcp://127.0.0.1:44741' processes=2 threads=2, memory=100.00 GB>"
+       "<Client: 'tcp://127.0.0.1:42607' processes=4 threads=4, memory=270.39 GB>"
       ]
      },
      "execution_count": 2,
@@ -114,18 +114,18 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 4,
+   "execution_count": 3,
    "metadata": {},
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "e95e03a0301344d0ae20857a659bfd34",
+       "model_id": "407a65d979fc43efa29f703b0b4f5eeb",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'data_gen'), ('type', 'ClassificationData'), ('conf', {'cl…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'data_gen'), ('type', 'ClassificationData'), ('conf', {…"
       ]
      },
      "metadata": {},
@@ -165,18 +165,18 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 5,
+   "execution_count": 4,
    "metadata": {},
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "7d0d0d15571341ee9564159a88be0db1",
+       "model_id": "84e8495bbf09401e9017f37af303c594",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', ''), ('type', 'Output_Collector'), ('conf', {}), ('inputs'…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', ''), ('type', 'Output_Collector'), ('conf', {}), ('inpu…"
       ]
      },
      "metadata": {},
@@ -208,18 +208,18 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 6,
+   "execution_count": 5,
    "metadata": {},
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "1caa94fded3c424db5b619d05173757b",
+       "model_id": "ddc534dac68041bf8c4820372bce40ed",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'data_gen'), ('type', 'ClassificationData'), ('conf', {'cl…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'data_gen'), ('type', 'ClassificationData'), ('conf', {…"
       ]
      },
      "metadata": {},
@@ -245,18 +245,18 @@
   },
   {
    "cell_type": "code",
-   "execution_count": 7,
+   "execution_count": 6,
    "metadata": {},
    "outputs": [
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "f1d8da9435864f7f9f4bb308c84d0711",
+       "model_id": "00a56b3501994211b3e4d4be6114345f",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'data_gen'), ('type', 'ClassificationData'), ('conf', {'cl…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'data_gen'), ('type', 'ClassificationData'), ('conf', {…"
       ]
      },
      "metadata": {},
@@ -270,7 +270,9 @@
   },
   {
    "cell_type": "markdown",
-   "metadata": {},
+   "metadata": {
+    "tags": []
+   },
    "source": [
     "### Forest inference for deployment\n",
     "Forest inference library provides a great performance boost for XGBoost model inference as shown in this [blog](https://medium.com/rapids-ai/rapids-forest-inference-library-prediction-at-100-million-rows-per-second-19558890bc35). In this step, we export the XGBoost model to use a forest inference library. \n",
@@ -291,12 +293,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "3d64b8d9b6fb4db989fa399790e75bd5",
+       "model_id": "3e1610d65ca743d49cb703109c3cfa9b",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'data_gen'), ('type', 'ClassificationData'), ('conf', {'cl…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'data_gen'), ('type', 'ClassificationData'), ('conf', {…"
       ]
      },
      "metadata": {},
@@ -341,12 +343,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "3f8e5a1daf7844b2973e75599f732830",
+       "model_id": "eb2dfdd169b7424c8c9667fd067681c9",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -379,12 +381,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "3b07f84823434e78939ebdae5dbbd58b",
+       "model_id": "6105da639ad04207a4c62a2eb30b2dd5",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'file…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'stock_data'), ('type', 'CsvStockLoader'), ('conf', {'f…"
       ]
      },
      "metadata": {},
@@ -463,7 +465,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.2"
+   "version": "3.8.8"
   }
  },
  "nbformat": 4,
diff --git a/gQuant/plugins/gquant_plugin/notebooks/09_gquant_machine_hpo.ipynb b/gQuant/plugins/gquant_plugin/notebooks/09_gquant_machine_hpo.ipynb
index 7d5fcf1e..ffc95af7 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/09_gquant_machine_hpo.ipynb
+++ b/gQuant/plugins/gquant_plugin/notebooks/09_gquant_machine_hpo.ipynb
@@ -41,22 +41,21 @@
      "name": "stderr",
      "output_type": "stream",
      "text": [
-      "2020-11-09 03:38:10,381\tINFO resource_spec.py:231 -- Starting Ray with 53.03 GiB memory available for workers and up to 26.54 GiB for objects. You can adjust these settings with ray.init(memory=<bytes>, object_store_memory=<bytes>).\n",
-      "2020-11-09 03:38:11,043\tINFO services.py:1193 -- View the Ray dashboard at \u001b[1m\u001b[32m172.17.0.2:8265\u001b[39m\u001b[22m\n",
-      "2020-11-09 03:38:11,048\tWARNING services.py:1567 -- WARNING: The object store is using /tmp instead of /dev/shm because /dev/shm has only 67100672 bytes available. This may slow down performance! You may be able to free up space by deleting files in /dev/shm or terminating any running plasma_store_server processes. If you are inside a Docker container, you may need to pass an argument with the flag '--shm-size' to 'docker run'.\n",
-      "2020-11-09 03:38:11,065\tWARNING services.py:1567 -- WARNING: The object store is using /tmp instead of /dev/shm because /dev/shm has only 67100672 bytes available. This may slow down performance! You may be able to free up space by deleting files in /dev/shm or terminating any running plasma_store_server processes. If you are inside a Docker container, you may need to pass an argument with the flag '--shm-size' to 'docker run'.\n"
+      "2021-03-08 14:45:07,029\tINFO services.py:1172 -- View the Ray dashboard at \u001b[1m\u001b[32mhttp://10.110.106.133:8265\u001b[39m\u001b[22m\n"
      ]
     },
     {
      "data": {
       "text/plain": [
-       "{'node_ip_address': '172.17.0.2',\n",
-       " 'raylet_ip_address': '172.17.0.2',\n",
-       " 'redis_address': '172.17.0.2:6379',\n",
-       " 'object_store_address': '/tmp/ray/session_2020-11-09_03-38-10_379948_6052/sockets/plasma_store',\n",
-       " 'raylet_socket_name': '/tmp/ray/session_2020-11-09_03-38-10_379948_6052/sockets/raylet',\n",
-       " 'webui_url': '172.17.0.2:8265',\n",
-       " 'session_dir': '/tmp/ray/session_2020-11-09_03-38-10_379948_6052'}"
+       "{'node_ip_address': '10.110.106.133',\n",
+       " 'raylet_ip_address': '10.110.106.133',\n",
+       " 'redis_address': '10.110.106.133:6379',\n",
+       " 'object_store_address': '/tmp/ray/session_2021-03-08_14-45-06_498135_26389/sockets/plasma_store',\n",
+       " 'raylet_socket_name': '/tmp/ray/session_2021-03-08_14-45-06_498135_26389/sockets/raylet',\n",
+       " 'webui_url': '10.110.106.133:8265',\n",
+       " 'session_dir': '/tmp/ray/session_2021-03-08_14-45-06_498135_26389',\n",
+       " 'metrics_export_port': 55861,\n",
+       " 'node_id': 'ac2f96849c51302a72b8eee9d843ddfb724eef0bb07c8ebc3af265ad'}"
       ]
      },
      "execution_count": 2,
@@ -84,12 +83,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "1ecd592caf8944d0aaea7391d1ce2337",
+       "model_id": "4f465102300c423ab8d28444148bad34",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'data_gen'), ('type', 'ClassificationData'), ('conf', {'n_…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'data_gen'), ('type', 'ClassificationData'), ('conf', {…"
       ]
      },
      "metadata": {},
@@ -126,12 +125,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "72f9ee997c1e4d93901178df7b1b606b",
+       "model_id": "21da6d292fb84618b6f87c7c81033196",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', ''), ('type', 'Output_Collector'), ('conf', {}), ('inputs'…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', ''), ('type', 'Output_Collector'), ('conf', {}), ('inpu…"
       ]
      },
      "metadata": {},
@@ -160,7 +159,12 @@
   {
    "cell_type": "code",
    "execution_count": 5,
-   "metadata": {},
+   "metadata": {
+    "jupyter": {
+     "source_hidden": true
+    },
+    "tags": []
+   },
    "outputs": [
     {
      "data": {
@@ -203,7 +207,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.7.7"
+   "version": "3.8.8"
   }
  },
  "nbformat": 4,
diff --git a/gQuant/plugins/gquant_plugin/notebooks/10_streamz.ipynb.ipynb b/gQuant/plugins/gquant_plugin/notebooks/10_streamz.ipynb.ipynb
index 15429083..3a9304df 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/10_streamz.ipynb.ipynb
+++ b/gQuant/plugins/gquant_plugin/notebooks/10_streamz.ipynb.ipynb
@@ -369,7 +369,7 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "d388c2b064f949fdb5be20ebe0717bdc",
+       "model_id": "02a6b5523ff84df096a8e0bf33d4921e",
        "version_major": 2,
        "version_minor": 0
       },
@@ -402,12 +402,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "0aabb8adc0d842eabdd3d38dd78eaca5",
+       "model_id": "c99c9b5d9d984b5b9955f5cd376b4d34",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'source'), ('type', 'StreamNode'), ('conf', {}), ('inputs'…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'source'), ('type', 'StreamNode'), ('conf', {}), ('inpu…"
       ]
      },
      "metadata": {},
@@ -681,12 +681,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "57bb637730d74bd19969a98aebc57b16",
+       "model_id": "bd84f6c6dd8d478b907f1f8e8122d220",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'source'), ('type', 'StreamNode'), ('conf', {}), ('inputs'…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'source'), ('type', 'StreamNode'), ('conf', {}), ('inpu…"
       ]
      },
      "metadata": {},
@@ -731,12 +731,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "2872e0f7dbe34d25804f03616ca90be0",
+       "model_id": "cfa9e22ddb9943e7aa1cbf22b5cce143",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "Figure(animation_duration=10, axes=[Axis(label='x', scale=LinearScale(), side='bottom'), Axis(label='y', orien…"
+       "Figure(animation_duration=10, axes=[Axis(label='x', scale=LinearScale()), Axis(label='y', orientation='vertica…"
       ]
      },
      "metadata": {},
@@ -880,12 +880,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "eab5330a56954bccb8ba78429bda9166",
+       "model_id": "e3401a8713a74a2095af9953e9bc9115",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'source'), ('type', 'StreamNode'), ('conf', {}), ('inputs'…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'source'), ('type', 'StreamNode'), ('conf', {}), ('inpu…"
       ]
      },
      "metadata": {},
@@ -930,12 +930,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "35326b6d4e6844baaf8618632a1f7773",
+       "model_id": "b6deeb96bc414366b924661ddea06184",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "Figure(animation_duration=10, axes=[Axis(label='x', scale=LinearScale(), side='bottom'), Axis(label='y', orien…"
+       "Figure(animation_duration=10, axes=[Axis(label='x', scale=LinearScale()), Axis(label='y', orientation='vertica…"
       ]
      },
      "metadata": {},
@@ -1274,12 +1274,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "2dcccf77e41e43429d97142fe23736e0",
+       "model_id": "8ab01b2f8604494390feae6f6eb76682",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'source'), ('type', 'StreamNode'), ('conf', {}), ('inputs'…"
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'source'), ('type', 'StreamNode'), ('conf', {}), ('inpu…"
       ]
      },
      "metadata": {},
@@ -1323,12 +1323,12 @@
     {
      "data": {
       "application/vnd.jupyter.widget-view+json": {
-       "model_id": "98df014eec71488abb30b271ea2c3fdf",
+       "model_id": "764a1a67426241faa8fd13203ce734fe",
        "version_major": 2,
        "version_minor": 0
       },
       "text/plain": [
-       "Figure(animation_duration=10, axes=[Axis(label='x', scale=LinearScale(), side='bottom'), Axis(label='y', orien…"
+       "Figure(animation_duration=10, axes=[Axis(label='x', scale=LinearScale()), Axis(label='y', orientation='vertica…"
       ]
      },
      "metadata": {},
@@ -4294,7 +4294,7 @@
    "name": "python",
    "nbconvert_exporter": "python",
    "pygments_lexer": "ipython3",
-   "version": "3.8.5"
+   "version": "3.8.8"
   }
  },
  "nbformat": 4,
diff --git a/gQuant/plugins/gquant_plugin/notebooks/11_simple_parallel.ipynb b/gQuant/plugins/gquant_plugin/notebooks/11_simple_parallel.ipynb
new file mode 100644
index 00000000..97c3d217
--- /dev/null
+++ b/gQuant/plugins/gquant_plugin/notebooks/11_simple_parallel.ipynb
@@ -0,0 +1,1059 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "id": "victorian-spelling",
+   "metadata": {},
+   "source": [
+    "## Simple Parallel Computation\n",
+    "\n",
+    "\n",
+    "In parallel computing, an [embarrassingly parallel workload](https://en.wikipedia.org/wiki/Embarrassingly_parallel) is one where little or no effort is needed to separate the problem into a number of parallel tasks. This is often the case where there is little or no dependency or need for communication between those parallel tasks. \n",
+    "\n",
+    "A lot of the time, data scientists are dealing with this type of workflow. They can focus on working on a single threaded computation graph. Greenflow provides a `SimpleParallelNode` to parallel it in the Dask. This notebook shows an simple example of it.\n",
+    "\n",
+    "Let's start the Dask cluster:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 1,
+   "id": "several-throw",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<table style=\"border: 2px solid white;\">\n",
+       "<tr>\n",
+       "<td style=\"vertical-align: top; border: 0px solid white\">\n",
+       "<h3 style=\"text-align: left;\">Client</h3>\n",
+       "<ul style=\"text-align: left; list-style: none; margin: 0; padding: 0;\">\n",
+       "  <li><b>Scheduler: </b>tcp://127.0.0.1:46799</li>\n",
+       "  <li><b>Dashboard: </b><a href='http://127.0.0.1:8787/status' target='_blank'>http://127.0.0.1:8787/status</a></li>\n",
+       "</ul>\n",
+       "</td>\n",
+       "<td style=\"vertical-align: top; border: 0px solid white\">\n",
+       "<h3 style=\"text-align: left;\">Cluster</h3>\n",
+       "<ul style=\"text-align: left; list-style:none; margin: 0; padding: 0;\">\n",
+       "  <li><b>Workers: </b>4</li>\n",
+       "  <li><b>Cores: </b>4</li>\n",
+       "  <li><b>Memory: </b>270.39 GB</li>\n",
+       "</ul>\n",
+       "</td>\n",
+       "</tr>\n",
+       "</table>"
+      ],
+      "text/plain": [
+       "<Client: 'tcp://127.0.0.1:46799' processes=4 threads=4, memory=270.39 GB>"
+      ]
+     },
+     "execution_count": 1,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "from dask_cuda import LocalCUDACluster\n",
+    "cluster = LocalCUDACluster()\n",
+    "from dask.distributed import Client\n",
+    "client = Client(cluster)\n",
+    "client"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "substantial-darwin",
+   "metadata": {},
+   "source": [
+    "We will use a simple taskgraph borrowed from `09_gquant_machine_leanring` notebook. It generates a random dataset and adds a few categorical variables to it."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 2,
+   "id": "diagnostic-trailer",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "ebb35c5f33bb41748e042b33289d68d4",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'data_gen'), ('type', 'ClassificationData'), ('conf', {…"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "from greenflow.dataframe_flow import TaskGraph\n",
+    "task_graph = TaskGraph.load_taskgraph('../taskgraphs/xgboost_example/data_generator.gq.yaml')\n",
+    "task_graph.draw()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "careful-special",
+   "metadata": {},
+   "source": [
+    "The above workflow is a typical embarrassingly parallel workload where we can generate a chunck of the datasets in different GPUs. \n",
+    "\n",
+    "Greenflow provides a `SimpleParallelNode` that can take a single GPU/CPU workflow, and convert it to output Dask dataframe. Each partition in the Dask dataframe will be computed in parallel in different GPU/CPUs. \n",
+    "\n",
+    "In the `SimpleParallelNode` configuration, user just need to set the taskgraph, the inputs and outputs of the taskgraph, the context parameters for the taskgraph. Most importantly, it can map the iteration id (or the Dask Dataframe partition id) to any number typed configuration item of the taskgraph. E.g. it can be used to set the randomn seed number for each of the iteration runs.\n",
+    "\n",
+    "In the following taskgraph, we will run the above workflow 4 times, each time it generates a 300 data points dataframe. We choose to get the outputs from two output ports. The `SimpleParallelNode` will combine the cudf dataframe result into the Dask Dataframe with 4 partitions."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 3,
+   "id": "returning-delaware",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "application/vnd.jupyter.widget-view+json": {
+       "model_id": "a17760f05fbb435a92af8cc384f8aa2c",
+       "version_major": 2,
+       "version_minor": 0
+      },
+      "text/plain": [
+       "GreenflowWidget(sub=HBox(), value=[OrderedDict([('id', 'paralell'), ('type', 'SimpleParallelNode'), ('conf', {…"
+      ]
+     },
+     "metadata": {},
+     "output_type": "display_data"
+    }
+   ],
+   "source": [
+    "task_graph = TaskGraph.load_taskgraph('../taskgraphs/xgboost_example/simple_parallel.gq.yaml')\n",
+    "task_graph.draw()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "seven-break",
+   "metadata": {},
+   "source": [
+    "It generates two Dask Dataframes of 4 partitions"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 4,
+   "id": "quality-services",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div><strong>Dask DataFrame Structure:</strong></div>\n",
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>x0</th>\n",
+       "      <th>x1</th>\n",
+       "      <th>x4</th>\n",
+       "      <th>x5</th>\n",
+       "      <th>x6</th>\n",
+       "      <th>x7</th>\n",
+       "      <th>x8</th>\n",
+       "      <th>x9</th>\n",
+       "      <th>y</th>\n",
+       "      <th>x3_0</th>\n",
+       "      <th>x3_1</th>\n",
+       "      <th>x2_0</th>\n",
+       "      <th>x2_1</th>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>npartitions=4</th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th></th>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>int64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th></th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th></th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th></th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th></th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>\n",
+       "<div>Dask Name: from-delayed, 12 tasks</div>"
+      ],
+      "text/plain": [
+       "<dask_cudf.DataFrame | 12 tasks | 4 npartitions>"
+      ]
+     },
+     "execution_count": 4,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "result = task_graph.run()\n",
+    "result['paralell.drop_x2_x3@out']\n"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 5,
+   "id": "saved-condition",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div><strong>Dask DataFrame Structure:</strong></div>\n",
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>x0</th>\n",
+       "      <th>x1</th>\n",
+       "      <th>x2</th>\n",
+       "      <th>x3</th>\n",
+       "      <th>x4</th>\n",
+       "      <th>x5</th>\n",
+       "      <th>x6</th>\n",
+       "      <th>x7</th>\n",
+       "      <th>x8</th>\n",
+       "      <th>x9</th>\n",
+       "      <th>y</th>\n",
+       "      <th>x2_sign</th>\n",
+       "      <th>x3_sign</th>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>npartitions=4</th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "      <th></th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th></th>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>float64</td>\n",
+       "      <td>int64</td>\n",
+       "      <td>int64</td>\n",
+       "      <td>int64</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th></th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th></th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th></th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th></th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "</div>\n",
+       "<div>Dask Name: from-delayed, 12 tasks</div>"
+      ],
+      "text/plain": [
+       "<dask_cudf.DataFrame | 12 tasks | 4 npartitions>"
+      ]
+     },
+     "execution_count": 5,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "result['paralell.x3_to_sign@out']"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "polar-palestinian",
+   "metadata": {},
+   "source": [
+    "We can evaluate them. Note, `SimpleParallelNode` calls `persist` on the output Dask Dataframes. `compute` won't re-compute the graph. "
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 6,
+   "id": "ranging-nicholas",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>x0</th>\n",
+       "      <th>x1</th>\n",
+       "      <th>x4</th>\n",
+       "      <th>x5</th>\n",
+       "      <th>x6</th>\n",
+       "      <th>x7</th>\n",
+       "      <th>x8</th>\n",
+       "      <th>x9</th>\n",
+       "      <th>y</th>\n",
+       "      <th>x3_0</th>\n",
+       "      <th>x3_1</th>\n",
+       "      <th>x2_0</th>\n",
+       "      <th>x2_1</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>-1.590499</td>\n",
+       "      <td>-0.109641</td>\n",
+       "      <td>-1.201097</td>\n",
+       "      <td>0.373905</td>\n",
+       "      <td>0.860087</td>\n",
+       "      <td>-0.008752</td>\n",
+       "      <td>0.415462</td>\n",
+       "      <td>0.425636</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>0.063491</td>\n",
+       "      <td>1.515215</td>\n",
+       "      <td>-0.461497</td>\n",
+       "      <td>-0.576744</td>\n",
+       "      <td>1.325481</td>\n",
+       "      <td>1.286008</td>\n",
+       "      <td>0.351541</td>\n",
+       "      <td>0.607517</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>-0.826528</td>\n",
+       "      <td>1.100968</td>\n",
+       "      <td>0.215652</td>\n",
+       "      <td>0.378117</td>\n",
+       "      <td>-0.325190</td>\n",
+       "      <td>0.210768</td>\n",
+       "      <td>1.526683</td>\n",
+       "      <td>-1.798531</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>4.000650</td>\n",
+       "      <td>2.296431</td>\n",
+       "      <td>0.399628</td>\n",
+       "      <td>-1.098406</td>\n",
+       "      <td>-1.673600</td>\n",
+       "      <td>1.775856</td>\n",
+       "      <td>-2.146905</td>\n",
+       "      <td>-0.302625</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>-1.642885</td>\n",
+       "      <td>-0.006057</td>\n",
+       "      <td>0.223185</td>\n",
+       "      <td>1.228473</td>\n",
+       "      <td>-0.877286</td>\n",
+       "      <td>0.455420</td>\n",
+       "      <td>0.364706</td>\n",
+       "      <td>0.096409</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>...</th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>295</th>\n",
+       "      <td>-0.867480</td>\n",
+       "      <td>-3.107077</td>\n",
+       "      <td>-0.404139</td>\n",
+       "      <td>0.802885</td>\n",
+       "      <td>-1.900449</td>\n",
+       "      <td>-2.221317</td>\n",
+       "      <td>-0.680082</td>\n",
+       "      <td>-0.136953</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>296</th>\n",
+       "      <td>-0.628308</td>\n",
+       "      <td>-1.522425</td>\n",
+       "      <td>-2.406415</td>\n",
+       "      <td>1.651044</td>\n",
+       "      <td>1.197216</td>\n",
+       "      <td>-1.236540</td>\n",
+       "      <td>-0.764979</td>\n",
+       "      <td>0.514345</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>297</th>\n",
+       "      <td>2.058393</td>\n",
+       "      <td>-0.069698</td>\n",
+       "      <td>0.297251</td>\n",
+       "      <td>-0.278816</td>\n",
+       "      <td>0.974293</td>\n",
+       "      <td>0.357927</td>\n",
+       "      <td>-1.581650</td>\n",
+       "      <td>1.008222</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>298</th>\n",
+       "      <td>0.278533</td>\n",
+       "      <td>-1.640604</td>\n",
+       "      <td>1.422526</td>\n",
+       "      <td>1.279790</td>\n",
+       "      <td>1.899960</td>\n",
+       "      <td>-0.236495</td>\n",
+       "      <td>-1.531827</td>\n",
+       "      <td>-0.210135</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>299</th>\n",
+       "      <td>-1.404608</td>\n",
+       "      <td>1.538087</td>\n",
+       "      <td>0.076346</td>\n",
+       "      <td>-0.272911</td>\n",
+       "      <td>1.048438</td>\n",
+       "      <td>0.353365</td>\n",
+       "      <td>-0.584258</td>\n",
+       "      <td>0.189324</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "      <td>0.0</td>\n",
+       "      <td>1.0</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "<p>1200 rows × 13 columns</p>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "           x0        x1        x4        x5        x6        x7        x8  \\\n",
+       "0   -1.590499 -0.109641 -1.201097  0.373905  0.860087 -0.008752  0.415462   \n",
+       "1    0.063491  1.515215 -0.461497 -0.576744  1.325481  1.286008  0.351541   \n",
+       "2   -0.826528  1.100968  0.215652  0.378117 -0.325190  0.210768  1.526683   \n",
+       "3    4.000650  2.296431  0.399628 -1.098406 -1.673600  1.775856 -2.146905   \n",
+       "4   -1.642885 -0.006057  0.223185  1.228473 -0.877286  0.455420  0.364706   \n",
+       "..        ...       ...       ...       ...       ...       ...       ...   \n",
+       "295 -0.867480 -3.107077 -0.404139  0.802885 -1.900449 -2.221317 -0.680082   \n",
+       "296 -0.628308 -1.522425 -2.406415  1.651044  1.197216 -1.236540 -0.764979   \n",
+       "297  2.058393 -0.069698  0.297251 -0.278816  0.974293  0.357927 -1.581650   \n",
+       "298  0.278533 -1.640604  1.422526  1.279790  1.899960 -0.236495 -1.531827   \n",
+       "299 -1.404608  1.538087  0.076346 -0.272911  1.048438  0.353365 -0.584258   \n",
+       "\n",
+       "           x9  y  x3_0  x3_1  x2_0  x2_1  \n",
+       "0    0.425636  1   1.0   0.0   1.0   0.0  \n",
+       "1    0.607517  0   1.0   0.0   1.0   0.0  \n",
+       "2   -1.798531  0   1.0   0.0   1.0   0.0  \n",
+       "3   -0.302625  0   1.0   0.0   1.0   0.0  \n",
+       "4    0.096409  1   1.0   0.0   1.0   0.0  \n",
+       "..        ... ..   ...   ...   ...   ...  \n",
+       "295 -0.136953  1   0.0   1.0   1.0   0.0  \n",
+       "296  0.514345  0   0.0   1.0   0.0   1.0  \n",
+       "297  1.008222  1   0.0   1.0   1.0   0.0  \n",
+       "298 -0.210135  1   0.0   1.0   0.0   1.0  \n",
+       "299  0.189324  0   0.0   1.0   0.0   1.0  \n",
+       "\n",
+       "[1200 rows x 13 columns]"
+      ]
+     },
+     "execution_count": 6,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "result['paralell.drop_x2_x3@out'].compute()"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 7,
+   "id": "numerical-sound",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/html": [
+       "<div>\n",
+       "<style scoped>\n",
+       "    .dataframe tbody tr th:only-of-type {\n",
+       "        vertical-align: middle;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe tbody tr th {\n",
+       "        vertical-align: top;\n",
+       "    }\n",
+       "\n",
+       "    .dataframe thead th {\n",
+       "        text-align: right;\n",
+       "    }\n",
+       "</style>\n",
+       "<table border=\"1\" class=\"dataframe\">\n",
+       "  <thead>\n",
+       "    <tr style=\"text-align: right;\">\n",
+       "      <th></th>\n",
+       "      <th>x0</th>\n",
+       "      <th>x1</th>\n",
+       "      <th>x2</th>\n",
+       "      <th>x3</th>\n",
+       "      <th>x4</th>\n",
+       "      <th>x5</th>\n",
+       "      <th>x6</th>\n",
+       "      <th>x7</th>\n",
+       "      <th>x8</th>\n",
+       "      <th>x9</th>\n",
+       "      <th>y</th>\n",
+       "      <th>x2_sign</th>\n",
+       "      <th>x3_sign</th>\n",
+       "    </tr>\n",
+       "  </thead>\n",
+       "  <tbody>\n",
+       "    <tr>\n",
+       "      <th>0</th>\n",
+       "      <td>-1.590499</td>\n",
+       "      <td>-0.109641</td>\n",
+       "      <td>-1.564527</td>\n",
+       "      <td>-1.738593</td>\n",
+       "      <td>-1.201097</td>\n",
+       "      <td>0.373905</td>\n",
+       "      <td>0.860087</td>\n",
+       "      <td>-0.008752</td>\n",
+       "      <td>0.415462</td>\n",
+       "      <td>0.425636</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>1</th>\n",
+       "      <td>0.063491</td>\n",
+       "      <td>1.515215</td>\n",
+       "      <td>-1.249759</td>\n",
+       "      <td>-0.217541</td>\n",
+       "      <td>-0.461497</td>\n",
+       "      <td>-0.576744</td>\n",
+       "      <td>1.325481</td>\n",
+       "      <td>1.286008</td>\n",
+       "      <td>0.351541</td>\n",
+       "      <td>0.607517</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>2</th>\n",
+       "      <td>-0.826528</td>\n",
+       "      <td>1.100968</td>\n",
+       "      <td>-1.211564</td>\n",
+       "      <td>-0.307162</td>\n",
+       "      <td>0.215652</td>\n",
+       "      <td>0.378117</td>\n",
+       "      <td>-0.325190</td>\n",
+       "      <td>0.210768</td>\n",
+       "      <td>1.526683</td>\n",
+       "      <td>-1.798531</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>3</th>\n",
+       "      <td>4.000650</td>\n",
+       "      <td>2.296431</td>\n",
+       "      <td>-0.584589</td>\n",
+       "      <td>-2.410844</td>\n",
+       "      <td>0.399628</td>\n",
+       "      <td>-1.098406</td>\n",
+       "      <td>-1.673600</td>\n",
+       "      <td>1.775856</td>\n",
+       "      <td>-2.146905</td>\n",
+       "      <td>-0.302625</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>4</th>\n",
+       "      <td>-1.642885</td>\n",
+       "      <td>-0.006057</td>\n",
+       "      <td>-1.400736</td>\n",
+       "      <td>-0.326703</td>\n",
+       "      <td>0.223185</td>\n",
+       "      <td>1.228473</td>\n",
+       "      <td>-0.877286</td>\n",
+       "      <td>0.455420</td>\n",
+       "      <td>0.364706</td>\n",
+       "      <td>0.096409</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>0</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>...</th>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "      <td>...</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>295</th>\n",
+       "      <td>-0.867480</td>\n",
+       "      <td>-3.107077</td>\n",
+       "      <td>-0.652265</td>\n",
+       "      <td>0.493787</td>\n",
+       "      <td>-0.404139</td>\n",
+       "      <td>0.802885</td>\n",
+       "      <td>-1.900449</td>\n",
+       "      <td>-2.221317</td>\n",
+       "      <td>-0.680082</td>\n",
+       "      <td>-0.136953</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>296</th>\n",
+       "      <td>-0.628308</td>\n",
+       "      <td>-1.522425</td>\n",
+       "      <td>0.779262</td>\n",
+       "      <td>1.245632</td>\n",
+       "      <td>-2.406415</td>\n",
+       "      <td>1.651044</td>\n",
+       "      <td>1.197216</td>\n",
+       "      <td>-1.236540</td>\n",
+       "      <td>-0.764979</td>\n",
+       "      <td>0.514345</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>297</th>\n",
+       "      <td>2.058393</td>\n",
+       "      <td>-0.069698</td>\n",
+       "      <td>-1.882360</td>\n",
+       "      <td>0.442483</td>\n",
+       "      <td>0.297251</td>\n",
+       "      <td>-0.278816</td>\n",
+       "      <td>0.974293</td>\n",
+       "      <td>0.357927</td>\n",
+       "      <td>-1.581650</td>\n",
+       "      <td>1.008222</td>\n",
+       "      <td>1</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>298</th>\n",
+       "      <td>0.278533</td>\n",
+       "      <td>-1.640604</td>\n",
+       "      <td>1.021632</td>\n",
+       "      <td>1.354946</td>\n",
+       "      <td>1.422526</td>\n",
+       "      <td>1.279790</td>\n",
+       "      <td>1.899960</td>\n",
+       "      <td>-0.236495</td>\n",
+       "      <td>-1.531827</td>\n",
+       "      <td>-0.210135</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "    </tr>\n",
+       "    <tr>\n",
+       "      <th>299</th>\n",
+       "      <td>-1.404608</td>\n",
+       "      <td>1.538087</td>\n",
+       "      <td>0.408563</td>\n",
+       "      <td>1.918054</td>\n",
+       "      <td>0.076346</td>\n",
+       "      <td>-0.272911</td>\n",
+       "      <td>1.048438</td>\n",
+       "      <td>0.353365</td>\n",
+       "      <td>-0.584258</td>\n",
+       "      <td>0.189324</td>\n",
+       "      <td>0</td>\n",
+       "      <td>1</td>\n",
+       "      <td>1</td>\n",
+       "    </tr>\n",
+       "  </tbody>\n",
+       "</table>\n",
+       "<p>1200 rows × 13 columns</p>\n",
+       "</div>"
+      ],
+      "text/plain": [
+       "           x0        x1        x2        x3        x4        x5        x6  \\\n",
+       "0   -1.590499 -0.109641 -1.564527 -1.738593 -1.201097  0.373905  0.860087   \n",
+       "1    0.063491  1.515215 -1.249759 -0.217541 -0.461497 -0.576744  1.325481   \n",
+       "2   -0.826528  1.100968 -1.211564 -0.307162  0.215652  0.378117 -0.325190   \n",
+       "3    4.000650  2.296431 -0.584589 -2.410844  0.399628 -1.098406 -1.673600   \n",
+       "4   -1.642885 -0.006057 -1.400736 -0.326703  0.223185  1.228473 -0.877286   \n",
+       "..        ...       ...       ...       ...       ...       ...       ...   \n",
+       "295 -0.867480 -3.107077 -0.652265  0.493787 -0.404139  0.802885 -1.900449   \n",
+       "296 -0.628308 -1.522425  0.779262  1.245632 -2.406415  1.651044  1.197216   \n",
+       "297  2.058393 -0.069698 -1.882360  0.442483  0.297251 -0.278816  0.974293   \n",
+       "298  0.278533 -1.640604  1.021632  1.354946  1.422526  1.279790  1.899960   \n",
+       "299 -1.404608  1.538087  0.408563  1.918054  0.076346 -0.272911  1.048438   \n",
+       "\n",
+       "           x7        x8        x9  y  x2_sign  x3_sign  \n",
+       "0   -0.008752  0.415462  0.425636  1        0        0  \n",
+       "1    1.286008  0.351541  0.607517  0        0        0  \n",
+       "2    0.210768  1.526683 -1.798531  0        0        0  \n",
+       "3    1.775856 -2.146905 -0.302625  0        0        0  \n",
+       "4    0.455420  0.364706  0.096409  1        0        0  \n",
+       "..        ...       ...       ... ..      ...      ...  \n",
+       "295 -2.221317 -0.680082 -0.136953  1        0        1  \n",
+       "296 -1.236540 -0.764979  0.514345  0        1        1  \n",
+       "297  0.357927 -1.581650  1.008222  1        0        1  \n",
+       "298 -0.236495 -1.531827 -0.210135  1        1        1  \n",
+       "299  0.353365 -0.584258  0.189324  0        1        1  \n",
+       "\n",
+       "[1200 rows x 13 columns]"
+      ]
+     },
+     "execution_count": 7,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "result['paralell.x3_to_sign@out'].compute()"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "id": "thick-gazette",
+   "metadata": {},
+   "source": [
+    "## Clean Up"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": 8,
+   "id": "stone-dress",
+   "metadata": {},
+   "outputs": [
+    {
+     "data": {
+      "text/plain": [
+       "{'status': 'ok', 'restart': True}"
+      ]
+     },
+     "execution_count": 8,
+     "metadata": {},
+     "output_type": "execute_result"
+    }
+   ],
+   "source": [
+    "import IPython\n",
+    "app = IPython.Application.instance()\n",
+    "app.kernel.do_shutdown(True)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "id": "funky-opposition",
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3",
+   "language": "python",
+   "name": "python3"
+  },
+  "language_info": {
+   "codemirror_mode": {
+    "name": "ipython",
+    "version": 3
+   },
+   "file_extension": ".py",
+   "mimetype": "text/x-python",
+   "name": "python",
+   "nbconvert_exporter": "python",
+   "pygments_lexer": "ipython3",
+   "version": "3.8.8"
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/accumulation_distribution.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/accumulation_distribution.py
index 4d4b9f9e..76990d0b 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/accumulation_distribution.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/accumulation_distribution.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import accumulation_distribution as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/admi.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/admi.py
index 9304f50e..1dacbfd5 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/admi.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/admi.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import average_directional_movement_index as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/average_true_range.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/average_true_range.py
index 0fa4508f..962a3d7a 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/average_true_range.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/average_true_range.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import average_true_range as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/bollinger_bands.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/bollinger_bands.py
index 1d1e8031..c1a52c61 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/bollinger_bands.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/bollinger_bands.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import bollinger_bands as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ch_oscillator.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ch_oscillator.py
index 1e2b3575..2cfbbe22 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ch_oscillator.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ch_oscillator.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import chaikin_oscillator as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/commodity_channel_index.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/commodity_channel_index.py
index 455995c7..334e6d9a 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/commodity_channel_index.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/commodity_channel_index.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import commodity_channel_index as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/coppock_curve.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/coppock_curve.py
index f6984a3e..3efe6ebd 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/coppock_curve.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/coppock_curve.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import coppock_curve as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/donchian_channel.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/donchian_channel.py
index bba5eb22..bdf47e17 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/donchian_channel.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/donchian_channel.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import donchian_channel as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ease_of_movement.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ease_of_movement.py
index 814cc67d..15774e74 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ease_of_movement.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ease_of_movement.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import ease_of_movement as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ewa.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ewa.py
index 83cf5820..887c4ee6 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ewa.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ewa.py
@@ -3,7 +3,7 @@
 from bqplot import Lines
 import math
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import exponential_moving_average as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/force_index.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/force_index.py
index 38560428..f517f3e5 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/force_index.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/force_index.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import force_index as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/keltner_channel.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/keltner_channel.py
index 1e697383..38c4066c 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/keltner_channel.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/keltner_channel.py
@@ -1,7 +1,7 @@
 import ipywidgets as widgets
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import keltner_channel as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/kst_oscillator.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/kst_oscillator.py
index 8e8bd3f6..fdfb6318 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/kst_oscillator.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/kst_oscillator.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import kst_oscillator as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ma.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ma.py
index 42313b87..18ed75e6 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ma.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ma.py
@@ -3,7 +3,7 @@
 from bqplot import Lines
 import math
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import moving_average as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/macd.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/macd.py
index 8ff92a73..bb806676 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/macd.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/macd.py
@@ -1,7 +1,7 @@
 import ipywidgets as widgets
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import macd as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/mass_index.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/mass_index.py
index fbc46c24..5e0a49a6 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/mass_index.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/mass_index.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import mass_index as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/momentum.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/momentum.py
index fcb32755..b6953de8 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/momentum.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/momentum.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import momentum as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/money_flow_index.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/money_flow_index.py
index 5ac312d5..347d18f0 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/money_flow_index.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/money_flow_index.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import money_flow_index as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/on_balance_volume.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/on_balance_volume.py
index ca6311d3..261911a2 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/on_balance_volume.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/on_balance_volume.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import on_balance_volume as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/parabolic_sar.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/parabolic_sar.py
index 34fcb41c..65e02ebb 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/parabolic_sar.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/parabolic_sar.py
@@ -1,7 +1,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import ppsr as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/rate_of_change.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/rate_of_change.py
index 9bfc772e..e1fa74c8 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/rate_of_change.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/rate_of_change.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import rate_of_change as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/rsi.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/rsi.py
index eef98eb9..21dabc98 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/rsi.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/rsi.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import relative_strength_index as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/stochastic_oscillator_d.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/stochastic_oscillator_d.py
index e03a48b2..47f35bc4 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/stochastic_oscillator_d.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/stochastic_oscillator_d.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import stochastic_oscillator_d as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/stochastic_oscillator_k.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/stochastic_oscillator_k.py
index 95ea262b..eb066b4b 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/stochastic_oscillator_k.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/stochastic_oscillator_k.py
@@ -1,7 +1,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import stochastic_oscillator_k as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/trix.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/trix.py
index d393bd51..46164f7a 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/trix.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/trix.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import exponential_moving_average as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/true_strength_index.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/true_strength_index.py
index ba06c095..29ccfbb2 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/true_strength_index.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/true_strength_index.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import true_strength_index as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ultimate_oscillator.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ultimate_oscillator.py
index 210d4e92..627bc8ba 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ultimate_oscillator.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/ultimate_oscillator.py
@@ -1,7 +1,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import ultimate_oscillator as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/vortex_indicator.py b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/vortex_indicator.py
index c658bcea..56306847 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/vortex_indicator.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/cuIndicator/viz/vortex_indicator.py
@@ -2,7 +2,7 @@
 from bqplot.colorschemes import CATEGORY20
 from bqplot import Axis, Figure, LinearScale, Lines
 import os
-from greenflow.dataframe_flow.task import load_modules
+from greenflow.dataframe_flow.config_nodes_modules import load_modules
 load_modules(os.getenv('MODULEPATH')+'/rapids_modules/')
 from rapids_modules.cuindicator import vortex_indicator as indicator_fun  # noqa #F401
 
diff --git a/gQuant/plugins/gquant_plugin/notebooks/custom_port_nodes.py b/gQuant/plugins/gquant_plugin/notebooks/custom_port_nodes.py
index 69f0ad90..d9339d7e 100644
--- a/gQuant/plugins/gquant_plugin/notebooks/custom_port_nodes.py
+++ b/gQuant/plugins/gquant_plugin/notebooks/custom_port_nodes.py
@@ -128,14 +128,14 @@ def meta_setup(self):
         input_meta = self.get_input_meta()
         output_cols = ({
                 'distance_df': {
-                    'distance_cudf': 'float64',
                     'x': 'float64',
-                    'y': 'float64'
+                    'y': 'float64',
+                    'distance_cudf': 'float64',
                 },
                 'distance_abs_df': {
-                    'distance_abs_cudf': 'float64',
                     'x': 'float64',
-                    'y': 'float64'
+                    'y': 'float64',
+                    'distance_abs_cudf': 'float64',
                 }
             })
         if 'points_df_in' in input_meta:
@@ -206,9 +206,9 @@ def meta_setup(self,):
         input_meta = self.get_input_meta()
         output_cols = ({
                 'distance_df': {
-                    'distance_numba': 'float64',
                     'x': 'float64',
-                    'y': 'float64'
+                    'y': 'float64',
+                    'distance_numba': 'float64',
                 }
             })
         if 'points_df_in' in input_meta:
@@ -297,9 +297,9 @@ def meta_setup(self,):
         input_meta = self.get_input_meta()
         output_cols = ({
                 'distance_df': {
-                    'distance_cupy': 'float64',
                     'x': 'float64',
-                    'y': 'float64'
+                    'y': 'float64',
+                    'distance_cupy': 'float64',
                 }
             })
         if 'points_df_in' in input_meta:
diff --git a/gQuant/plugins/gquant_plugin/setup.py b/gQuant/plugins/gquant_plugin/setup.py
index 613ae486..21a2adec 100644
--- a/gQuant/plugins/gquant_plugin/setup.py
+++ b/gQuant/plugins/gquant_plugin/setup.py
@@ -2,28 +2,29 @@
 
 setup(
     name='greenflow_gquant_plugin',
-    version='0.0.1',
+    version='0.0.2',
     install_requires=[
-        "bqplot", "tables", "ray[tune]"
+        "greenflow", "bqplot", "tables", "ray[tune]", "matplotlib", "ray[default]",
+        "mplfinance"
     ],
-    packages=find_packages(include=['greenflow_gquant_plugin',
-                                    'greenflow_gquant_plugin.analysis',
-                                    'greenflow_gquant_plugin.backtest',
-                                    'greenflow_gquant_plugin.dataloader',
-                                    'greenflow_gquant_plugin.ml',
-                                    'greenflow_gquant_plugin.portofolio',
-                                    'greenflow_gquant_plugin.strategy',
-                                    'greenflow_gquant_plugin.cuindicator',
-                                    'greenflow_gquant_plugin.transform']),
+    packages=find_packages(include=[
+        'greenflow_gquant_plugin', 'greenflow_gquant_plugin.analysis',
+        'greenflow_gquant_plugin.backtest',
+        'greenflow_gquant_plugin.dataloader', 'greenflow_gquant_plugin.ml',
+        'greenflow_gquant_plugin.portofolio',
+        'greenflow_gquant_plugin.strategy',
+        'greenflow_gquant_plugin.cuindicator',
+        'greenflow_gquant_plugin.transform'
+    ]),
     entry_points={
-        'greenflow.plugin':
-        ['greenflow_gquant_plugin = greenflow_gquant_plugin',
-         'greenflow_gquant_plugin.analysis = greenflow_gquant_plugin.analysis',
-         'greenflow_gquant_plugin.backtest = greenflow_gquant_plugin.backtest',
-         'greenflow_gquant_plugin.dataloader = greenflow_gquant_plugin.dataloader',
-         'greenflow_gquant_plugin.ml = greenflow_gquant_plugin.ml',
-         'greenflow_gquant_plugin.portofolio = greenflow_gquant_plugin.portofolio',
-         'greenflow_gquant_plugin.strategy = greenflow_gquant_plugin.strategy',
-         'greenflow_gquant_plugin.transform = greenflow_gquant_plugin.transform'],
-    }
-)
+        'greenflow.plugin': [
+            'greenflow_gquant_plugin = greenflow_gquant_plugin',
+            'greenflow_gquant_plugin.analysis = greenflow_gquant_plugin.analysis',
+            'greenflow_gquant_plugin.backtest = greenflow_gquant_plugin.backtest',
+            'greenflow_gquant_plugin.dataloader = greenflow_gquant_plugin.dataloader',
+            'greenflow_gquant_plugin.ml = greenflow_gquant_plugin.ml',
+            'greenflow_gquant_plugin.portofolio = greenflow_gquant_plugin.portofolio',
+            'greenflow_gquant_plugin.strategy = greenflow_gquant_plugin.strategy',
+            'greenflow_gquant_plugin.transform = greenflow_gquant_plugin.transform'
+        ],
+    })
diff --git a/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/data_generator.gq.yaml b/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/data_generator.gq.yaml
index 0a10d3bc..d66d2575 100644
--- a/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/data_generator.gq.yaml
+++ b/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/data_generator.gq.yaml
@@ -1,73 +1,74 @@
-- conf:
-    class_sep: 1
-    dtype: float64
-    flip_y: 0.01
-    hypercube: true
-    n_classes: 2
-    n_clusters_per_class: 2
+- id: data_gen
+  type: ClassificationData
+  conf:
+    n_samples: 10000
     n_features: 10
     n_informative: 4
-    n_parts: 4
     n_redundant: 0
     n_repeated: 0
-    n_samples: 10000
-    order: F
-    scale: 1
+    n_classes: 2
+    n_clusters_per_class: 2
+    flip_y: 0.01
+    class_sep: 1
+    hypercube: true
     shift: 0
+    scale: 1
     shuffle: true
-  id: data_gen
+    order: F
+    dtype: float64
+    n_parts: 4
+    random_state: 0
   inputs: {}
   module: greenflow_gquant_plugin.dataloader
-  type: ClassificationData
-- conf: {}
-  id: ''
+- id: ''
+  type: Output_Collector
+  conf: {}
   inputs:
     in1: drop_x2_x3.out
-  type: Output_Collector
-- conf:
+- id: x2_to_sign
+  type: AddSignIndicatorNode
+  conf:
     column: x2
     sign: x2_sign
-  id: x2_to_sign
   inputs:
     in: data_gen.cudf_out
   module: greenflow_gquant_plugin.transform
+- id: x3_to_sign
   type: AddSignIndicatorNode
-- conf:
+  conf:
     column: x3
     sign: x3_sign
-  id: x3_to_sign
   inputs:
     in: x2_to_sign.out
   module: greenflow_gquant_plugin.transform
-  type: AddSignIndicatorNode
-- conf:
+- id: drop_x2_x3
+  type: DropNode
+  conf:
     columns:
-    - x2
-    - x3
-    - x3_sign
-    - x2_sign
-  id: drop_x2_x3
+      - x2
+      - x3
+      - x3_sign
+      - x2_sign
   inputs:
     in: one_hot_encoding.out
   module: greenflow_gquant_plugin.transform
-  type: DropNode
-- conf:
-  - cats:
-    - 0
-    - 1
-    column: x3_sign
-    dtype: float64
-    prefix: x3
-    prefix_sep: _
-  - cats:
-    - 0
-    - 1
-    column: x2_sign
-    dtype: float64
-    prefix: x2
-    prefix_sep: _
-  id: one_hot_encoding
+- id: one_hot_encoding
+  type: OneHotEncodingNode
+  conf:
+    - cats:
+        - 0
+        - 1
+      column: x3_sign
+      dtype: float64
+      prefix: x3
+      prefix_sep: _
+    - cats:
+        - 0
+        - 1
+      column: x2_sign
+      dtype: float64
+      prefix: x2
+      prefix_sep: _
   inputs:
     in: x3_to_sign.out
   module: greenflow_gquant_plugin.transform
-  type: OneHotEncodingNode
diff --git a/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/hyper_parameters_search.gq.yaml b/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/hyper_parameters_search.gq.yaml
index a87fc925..f8749554 100644
--- a/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/hyper_parameters_search.gq.yaml
+++ b/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/hyper_parameters_search.gq.yaml
@@ -1,4 +1,6 @@
-- conf:
+- id: data_gen
+  type: ClassificationData
+  conf:
     class_sep: 1
     dtype: float64
     flip_y: 0.01
@@ -15,26 +17,26 @@
     scale: 1
     shift: 0
     shuffle: true
-  id: data_gen
   inputs: {}
   module: greenflow_gquant_plugin.dataloader
-  type: ClassificationData
-- conf: {}
-  id: ''
+- id: ''
+  type: Output_Collector
+  conf: {}
   inputs:
     in1: xgboost_model.train_roc@roc_curve
     in2: xgboost_model.test_roc@roc_curve
     in3: hpo.conf_out
     in4: hpo.train_roc@roc_curve
     in5: hpo.test_roc@roc_curve
-  type: Output_Collector
-- conf:
+- id: data_generator
+  type: CompositeNode
+  conf:
     input:
-    - x2_to_sign.in
+      - x2_to_sign.in
     output:
-    - drop_x2_x3.out
+      - drop_x2_x3.out
     subnode_ids:
-    - data_gen
+      - data_gen
     subnodes_conf:
       data_gen:
         conf:
@@ -56,143 +58,143 @@
           shift: 0
           shuffle: true
     taskgraph: taskgraphs/xgboost_example/data_generator.gq.yaml
-  id: data_generator
   inputs:
     x2_to_sign@in: data_gen.cudf_out
-  type: CompositeNode
-- conf:
-    target: y
+- id: data_splitter
+  type: DataSplittingNode
+  conf:
+    target: 'y'
     train_size: 0.8
-  id: data_splitter
   inputs:
     in: data_generator.drop_x2_x3@out
   module: greenflow_gquant_plugin.ml
-  type: DataSplittingNode
-- conf:
+- id: xgboost_model
+  type: ContextCompositeNode
+  conf:
     context:
       depth:
         map:
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.xgboost_parameters.max_depth
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.xgboost_parameters.max_depth
         type: number
         value: 1
       eta:
         map:
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.xgboost_parameters.eta
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.xgboost_parameters.eta
         type: number
         value: 0.1
       features:
         map:
-        - node_id: train_norm
-          xpath: train_norm.conf.columns
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.columns
+          - node_id: train_norm
+            xpath: train_norm.conf.columns
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.columns
         type: array_string
         value:
-        - y
+          - 'y'
       inclusive:
         map:
-        - node_id: train_norm
-          xpath: train_norm.conf.include
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.include
+          - node_id: train_norm
+            xpath: train_norm.conf.include
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.include
         type: boolean
         value: false
       target:
         map:
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.target
-        - node_id: train_roc
-          xpath: train_roc.conf.label
-        - node_id: test_roc
-          xpath: test_roc.conf.label
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.target
+          - node_id: train_roc
+            xpath: train_roc.conf.label
+          - node_id: test_roc
+            xpath: test_roc.conf.label
         type: string
-        value: y
+        value: 'y'
     input:
-    - train_norm.df_in
-    - test_norm.df_in
+      - train_norm.df_in
+      - test_norm.df_in
     output:
-    - train_infer.out
-    - test_infer.out
-    - train_roc.roc_curve
-    - test_roc.roc_curve
+      - train_infer.out
+      - test_infer.out
+      - train_roc.roc_curve
+      - test_roc.roc_curve
     subnodes_conf: {}
     taskgraph: taskgraphs/xgboost_example/xgboost_model_with_metrics.gq.yaml
-  id: xgboost_model
   inputs:
-    test_norm@df_in: data_splitter.test
-    train_norm@df_in: data_splitter.train
-  type: ContextCompositeNode
-- conf:
+    train_norm@df_in: persist.data_splitter@train
+    test_norm@df_in: persist.data_splitter@test
+- id: hpo
+  type: GridRandomSearchNode
+  conf:
     best:
       metric: test_roc.value
       mode: max
     context:
       depth:
         map:
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.xgboost_parameters.max_depth
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.xgboost_parameters.max_depth
         type: number
-        value: 3
+        value: 1
       eta:
         map:
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.xgboost_parameters.eta
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.xgboost_parameters.eta
         type: number
-        value: 0.2719547419145216
+        value: 0.1
       features:
         map:
-        - node_id: train_norm
-          xpath: train_norm.conf.columns
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.columns
+          - node_id: train_norm
+            xpath: train_norm.conf.columns
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.columns
         type: array_string
         value:
-        - y
+          - 'y'
       inclusive:
         map:
-        - node_id: train_norm
-          xpath: train_norm.conf.include
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.include
+          - node_id: train_norm
+            xpath: train_norm.conf.include
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.include
         type: boolean
         value: false
       target:
         map:
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.target
-        - node_id: train_roc
-          xpath: train_roc.conf.label
-        - node_id: test_roc
-          xpath: test_roc.conf.label
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.target
+          - node_id: train_roc
+            xpath: train_roc.conf.label
+          - node_id: test_roc
+            xpath: test_roc.conf.label
         type: string
-        value: y
+        value: 'y'
     input:
-    - train_norm.df_in
-    - test_norm.df_in
+      - train_norm.df_in
+      - test_norm.df_in
     metrics:
-    - train_roc.value
-    - test_roc.value
+      - train_roc.value
+      - test_roc.value
     output:
-    - train_infer.out
-    - test_infer.out
-    - train_roc.roc_curve
-    - test_roc.roc_curve
+      - train_infer.out
+      - test_infer.out
+      - train_roc.roc_curve
+      - test_roc.roc_curve
     parameters:
-    - name: depth
-      search:
-        args:
-        - 1
-        - 3
-        - 5
-        function: grid_search
-    - name: eta
-      search:
-        args:
-        - 0.1
-        - 0.8
-        function: uniform
+      - name: depth
+        search:
+          args:
+            - 1
+            - 3
+            - 5
+          function: grid_search
+      - name: eta
+        search:
+          args:
+            - 0.1
+            - 0.8
+          function: uniform
     subnodes_conf: {}
     taskgraph: taskgraphs/xgboost_example/xgboost_model_with_metrics.gq.yaml
     tune:
@@ -202,10 +204,15 @@
       resources_per_trial:
         cpu: 1
         gpu: 1
-  id: hpo
   inputs:
     conf_in: xgboost_model.conf_out
-    test_norm@df_in: data_splitter.test
-    train_norm@df_in: data_splitter.train
+    train_norm@df_in: persist.data_splitter@train
+    test_norm@df_in: persist.data_splitter@test
   module: greenflow_gquant_plugin.ml
-  type: GridRandomSearchNode
+- id: persist
+  type: PersistNode
+  conf: {}
+  inputs:
+    data_splitter@test: data_splitter.test
+    data_splitter@train: data_splitter.train
+  module: greenflow_dask_plugin
diff --git a/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/simple_parallel.gq.yaml b/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/simple_parallel.gq.yaml
new file mode 100644
index 00000000..da2435ed
--- /dev/null
+++ b/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/simple_parallel.gq.yaml
@@ -0,0 +1,26 @@
+- id: paralell
+  type: SimpleParallelNode
+  conf:
+    output:
+      - drop_x2_x3.out
+      - x3_to_sign.out
+    context:
+      num_points:
+        type: number
+        map:
+          - node_id: data_gen
+            xpath: data_gen.conf.n_samples
+        value: 300
+    map:
+      - node_id: data_gen
+        xpath: data_gen.conf.random_state
+    taskgraph: taskgraphs/xgboost_example/data_generator.gq.yaml
+    iterations: 4
+  inputs: {}
+  module: greenflow_dask_plugin
+- id: ''
+  type: Output_Collector
+  conf: {}
+  inputs:
+    in1: paralell.drop_x2_x3@out
+    in2: paralell.x3_to_sign@out
diff --git a/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/tree_inference.gq.yaml b/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/tree_inference.gq.yaml
index 2b733a1d..d0a6d287 100644
--- a/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/tree_inference.gq.yaml
+++ b/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/tree_inference.gq.yaml
@@ -24,9 +24,9 @@
   conf: {}
   inputs:
     in1: model_export.filename
-    in2: test_data.out
-    in3: test_infer.out
-    in4: xgboost_infer.out
+    in2: compute.xgboost_model@test_infer@out
+    in3: compute.xgboost_model@test_norm@df_out
+    in4: compute.tree_inference@out
 - id: data_generator
   type: CompositeNode
   conf:
@@ -104,28 +104,18 @@
     data_in: xgboost_model.test_norm@df_out
     model_file: model_export.filename
   module: greenflow_gquant_plugin.ml
-- id: test_data
+- id: compute
   type: DaskComputeNode
   conf: {}
   inputs:
-    in: xgboost_model.test_norm@df_out
-  module: greenflow_gquant_plugin.transform
-- id: test_infer
-  type: DaskComputeNode
-  conf: {}
-  inputs:
-    in: tree_inference.out
-  module: greenflow_gquant_plugin.transform
-- id: xgboost_infer
-  type: DaskComputeNode
-  conf: {}
-  inputs:
-    in: xgboost_model.test_infer@out
-  module: greenflow_gquant_plugin.transform
+    xgboost_model@test_infer@out: xgboost_model.test_infer@out
+    xgboost_model@test_norm@df_out: xgboost_model.test_norm@df_out
+    tree_inference@out: tree_inference.out
+  module: greenflow_dask_plugin
 - id: persist
   type: PersistNode
   conf: {}
   inputs:
     data_splitter@test: data_splitter.test
     data_splitter@train: data_splitter.train
-  module: greenflow_gquant_plugin.transform
+  module: greenflow_dask_plugin
diff --git a/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/xgboost_stock.gq.yaml b/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/xgboost_stock.gq.yaml
index 13038a80..f84833f0 100644
--- a/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/xgboost_stock.gq.yaml
+++ b/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/xgboost_stock.gq.yaml
@@ -1,253 +1,255 @@
-- conf:
+- id: stock_data
+  type: CsvStockLoader
+  conf:
     file: notebooks/data/stock_price_hist.csv.gz
     path: notebooks/many-small
-  id: stock_data
   inputs: {}
   module: greenflow_gquant_plugin.dataloader
-  type: CsvStockLoader
-- conf: {}
-  id: ''
+- id: ''
+  type: Output_Collector
+  conf: {}
   inputs:
     in1: test_roc.roc_curve
     in2: train_roc.roc_curve
     in3: feature_importance.importance_curve
     in4: xgboost_model.train_xgboost@model_out
-  type: Output_Collector
-- conf:
+- id: stock_feature
+  type: CompositeNode
+  conf:
     input:
-    - preprocess.sort_node@in
+      - preprocess.sort_node@in
     output:
-    - technical_indicator.stock_out
+      - technical_indicator.stock_out
     subnode_ids:
-    - technical_indicator
+      - technical_indicator
     subnodes_conf:
       technical_indicator:
         conf:
           indicators:
-          - args:
-            - 10
-            columns:
-            - close
-            function: port_bollinger_bands
-          - args:
-            - 2
-            - 3
-            columns:
-            - high
-            - low
-            - close
-            - volume
-            function: port_chaikin_oscillator
-          - args:
-            - 2
-            - 3
-            columns:
-            - close
-            function: port_macd
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            function: port_relative_strength_index
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_average_true_range
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_stochastic_oscillator_k
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_stochastic_oscillator_d
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            - volume
-            function: port_money_flow_index
-          - args:
-            - 2
-            columns:
-            - close
-            - volume
-            function: port_force_index
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_ultimate_oscillator
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            - volume
-            function: port_accumulation_distribution
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_commodity_channel_index
-          - args:
-            - 2
-            columns:
-            - close
-            - volume
-            function: port_on_balance_volume
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_vortex_indicator
-          - args:
-            - 3
-            - 4
-            - 5
-            - 6
-            - 7
-            - 8
-            - 9
-            - 10
-            columns:
-            - close
-            function: port_kst_oscillator
-          - args:
-            - 2
-            - 3
-            columns:
-            - high
-            - low
-            function: port_mass_index
-          - args:
-            - 2
-            - 3
-            columns:
-            - close
-            function: port_true_strength_index
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - volume
-            function: port_ease_of_movement
-          - args:
-            - 2
-            columns:
-            - close
-            function: port_coppock_curve
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_keltner_channel
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_ppsr
-          - args:
-            - 0.9
-            columns:
-            - close
-            function: port_fractional_diff
-          - args:
-            - 0.7
-            columns:
-            - close
-            function: port_fractional_diff
-          - args:
-            - 0.5
-            columns:
-            - close
-            function: port_fractional_diff
-          - args:
-            - 0.3
-            columns:
-            - close
-            function: port_fractional_diff
-          - args:
-            - 0.1
-            columns:
-            - close
-            function: port_fractional_diff
-          - args:
-            - -1
-            columns:
-            - returns
-            function: port_shift
+            - args:
+                - 10
+              columns:
+                - close
+              function: port_bollinger_bands
+            - args:
+                - 2
+                - 3
+              columns:
+                - high
+                - low
+                - close
+                - volume
+              function: port_chaikin_oscillator
+            - args:
+                - 2
+                - 3
+              columns:
+                - close
+              function: port_macd
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+              function: port_relative_strength_index
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_average_true_range
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_stochastic_oscillator_k
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_stochastic_oscillator_d
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+                - volume
+              function: port_money_flow_index
+            - args:
+                - 2
+              columns:
+                - close
+                - volume
+              function: port_force_index
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_ultimate_oscillator
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+                - volume
+              function: port_accumulation_distribution
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_commodity_channel_index
+            - args:
+                - 2
+              columns:
+                - close
+                - volume
+              function: port_on_balance_volume
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_vortex_indicator
+            - args:
+                - 3
+                - 4
+                - 5
+                - 6
+                - 7
+                - 8
+                - 9
+                - 10
+              columns:
+                - close
+              function: port_kst_oscillator
+            - args:
+                - 2
+                - 3
+              columns:
+                - high
+                - low
+              function: port_mass_index
+            - args:
+                - 2
+                - 3
+              columns:
+                - close
+              function: port_true_strength_index
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - volume
+              function: port_ease_of_movement
+            - args:
+                - 2
+              columns:
+                - close
+              function: port_coppock_curve
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_keltner_channel
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_ppsr
+            - args:
+                - 0.9
+              columns:
+                - close
+              function: port_fractional_diff
+            - args:
+                - 0.7
+              columns:
+                - close
+              function: port_fractional_diff
+            - args:
+                - 0.5
+              columns:
+                - close
+              function: port_fractional_diff
+            - args:
+                - 0.3
+              columns:
+                - close
+              function: port_fractional_diff
+            - args:
+                - 0.1
+              columns:
+                - close
+              function: port_fractional_diff
+            - args:
+                - -1
+              columns:
+                - returns
+              function: port_shift
           remove_na: true
     taskgraph: taskgraphs/xgboost_trade.gq.yaml
-  id: stock_feature
   inputs:
     preprocess@sort_node@in: stock_data.dask_cudf_out
-  type: CompositeNode
-- conf:
+- id: pos_neg_return
+  type: AddSignIndicatorNode
+  conf:
     column: SHIFT_-1
     sign: sign
-  id: pos_neg_return
   inputs:
     in: stock_feature.technical_indicator@stock_out
   module: greenflow_gquant_plugin.transform
-  type: AddSignIndicatorNode
-- conf:
+- id: drop_col
+  type: DropNode
+  conf:
     columns:
-    - indicator
-    - datetime
-    - asset
-    - SHIFT_-1
-    - open
-    - high
-    - low
-    - close
-  id: drop_col
+      - indicator
+      - datetime
+      - asset
+      - SHIFT_-1
+      - open
+      - high
+      - low
+      - close
   inputs:
     in: pos_neg_return.out
   module: greenflow_gquant_plugin.transform
-  type: DropNode
-- conf:
+- id: split_data
+  type: DataSplittingNode
+  conf:
     target: sign
     train_size: 0.8
-  id: split_data
   inputs:
     in: drop_col.out
   module: greenflow_gquant_plugin.ml
-  type: DataSplittingNode
-- conf:
+- id: xgboost_model
+  type: CustXGBoostNode
+  conf:
     train_norm:
       conf:
         columns:
-        - sign
+          - sign
         include: false
     train_xgboost:
       conf:
         columns:
-        - sign
+          - sign
         include: false
         num_of_rounds: 100
         target: sign
@@ -264,37 +266,42 @@
           max_depth: 8
           max_leaves: 256
           min_child_weight: 1
-          objective: binary:logistic
+          objective: 'binary:logistic'
           sampling_method: uniform
           single_precision_histogram: false
           subsample: 1
           tree_method: gpu_hist
-  id: xgboost_model
   inputs:
-    test_norm@df_in: split_data.test
-    train_norm@df_in: split_data.train
+    train_norm@df_in: persist.split_data@train
+    test_norm@df_in: persist.split_data@test
   module: my_node
-  type: CustXGBoostNode
-- conf:
+- id: train_roc
+  type: RocCurveNode
+  conf:
     label: sign
     prediction: predict
-  id: train_roc
   inputs:
     in: xgboost_model.train_infer@out
   module: greenflow_gquant_plugin.analysis
+- id: test_roc
   type: RocCurveNode
-- conf:
+  conf:
     label: sign
     prediction: predict
-  id: test_roc
   inputs:
     in: xgboost_model.test_infer@out
   module: greenflow_gquant_plugin.analysis
-  type: RocCurveNode
-- conf:
+- id: feature_importance
+  type: ImportanceCurveNode
+  conf:
     type: gain
-  id: feature_importance
   inputs:
     in: xgboost_model.train_xgboost@model_out
   module: greenflow_gquant_plugin.analysis
-  type: ImportanceCurveNode
+- id: persist
+  type: PersistNode
+  conf: {}
+  inputs:
+    split_data@train: split_data.train
+    split_data@test: split_data.test
+  module: greenflow_dask_plugin
diff --git a/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/xgboost_stock_hpo.gq.yaml b/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/xgboost_stock_hpo.gq.yaml
index c3e4e3d9..a1f365e7 100644
--- a/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/xgboost_stock_hpo.gq.yaml
+++ b/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_example/xgboost_stock_hpo.gq.yaml
@@ -1,137 +1,139 @@
-- conf: {}
-  id: ''
+- id: ''
+  type: Output_Collector
+  conf: {}
   inputs:
     in1: xgboost_model.train_roc@roc_curve
     in2: xgboost_model.test_roc@roc_curve
     in3: hpo.conf_out
     in4: hpo.train_roc@roc_curve
     in5: hpo.test_roc@roc_curve
-  type: Output_Collector
-- conf:
+- id: xgboost_model
+  type: ContextCompositeNode
+  conf:
     context:
       depth:
         map:
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.xgboost_parameters.max_depth
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.xgboost_parameters.max_depth
         type: number
         value: 1
       eta:
         map:
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.xgboost_parameters.eta
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.xgboost_parameters.eta
         type: number
         value: 0.1
       features:
         map:
-        - node_id: train_norm
-          xpath: train_norm.conf.columns
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.columns
+          - node_id: train_norm
+            xpath: train_norm.conf.columns
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.columns
         type: array_string
         value:
-        - sign
+          - sign
       inclusive:
         map:
-        - node_id: train_norm
-          xpath: train_norm.conf.include
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.include
+          - node_id: train_norm
+            xpath: train_norm.conf.include
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.include
         type: boolean
         value: false
       target:
         map:
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.target
-        - node_id: train_roc
-          xpath: train_roc.conf.label
-        - node_id: test_roc
-          xpath: test_roc.conf.label
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.target
+          - node_id: train_roc
+            xpath: train_roc.conf.label
+          - node_id: test_roc
+            xpath: test_roc.conf.label
         type: string
         value: sign
     input:
-    - train_norm.df_in
-    - test_norm.df_in
+      - train_norm.df_in
+      - test_norm.df_in
     output:
-    - train_infer.out
-    - test_infer.out
-    - train_roc.roc_curve
-    - test_roc.roc_curve
+      - train_infer.out
+      - test_infer.out
+      - train_roc.roc_curve
+      - test_roc.roc_curve
     subnodes_conf: {}
     taskgraph: taskgraphs/xgboost_example/xgboost_model_with_metrics.gq.yaml
-  id: xgboost_model
   inputs:
-    test_norm@df_in: split_data.test
-    train_norm@df_in: split_data.train
-  type: ContextCompositeNode
-- conf:
+    train_norm@df_in: persist.split_data@train
+    test_norm@df_in: persist.split_data@test
+- id: hpo
+  type: GridRandomSearchNode
+  conf:
     best:
       metric: test_roc.value
       mode: max
     context:
       depth:
         map:
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.xgboost_parameters.max_depth
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.xgboost_parameters.max_depth
         type: number
         value: 1
       eta:
         map:
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.xgboost_parameters.eta
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.xgboost_parameters.eta
         type: number
         value: 0.1
       features:
         map:
-        - node_id: train_norm
-          xpath: train_norm.conf.columns
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.columns
+          - node_id: train_norm
+            xpath: train_norm.conf.columns
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.columns
         type: array_string
         value:
-        - sign
+          - sign
       inclusive:
         map:
-        - node_id: train_norm
-          xpath: train_norm.conf.include
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.include
+          - node_id: train_norm
+            xpath: train_norm.conf.include
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.include
         type: boolean
         value: false
       target:
         map:
-        - node_id: train_xgboost
-          xpath: train_xgboost.conf.target
-        - node_id: train_roc
-          xpath: train_roc.conf.label
-        - node_id: test_roc
-          xpath: test_roc.conf.label
+          - node_id: train_xgboost
+            xpath: train_xgboost.conf.target
+          - node_id: train_roc
+            xpath: train_roc.conf.label
+          - node_id: test_roc
+            xpath: test_roc.conf.label
         type: string
         value: sign
     input:
-    - train_norm.df_in
-    - test_norm.df_in
+      - train_norm.df_in
+      - test_norm.df_in
     metrics:
-    - train_roc.value
-    - test_roc.value
+      - train_roc.value
+      - test_roc.value
     output:
-    - train_infer.out
-    - test_infer.out
-    - train_roc.roc_curve
-    - test_roc.roc_curve
+      - train_infer.out
+      - test_infer.out
+      - train_roc.roc_curve
+      - test_roc.roc_curve
     parameters:
-    - name: depth
-      search:
-        args:
-        - 1
-        - 3
-        - 5
-        function: grid_search
-    - name: eta
-      search:
-        args:
-        - 0.1
-        - 0.8
-        function: uniform
+      - name: depth
+        search:
+          args:
+            - 1
+            - 3
+            - 5
+          function: grid_search
+      - name: eta
+        search:
+          args:
+            - 0.1
+            - 0.8
+          function: uniform
     subnodes_conf: {}
     taskgraph: taskgraphs/xgboost_example/xgboost_model_with_metrics.gq.yaml
     tune:
@@ -141,242 +143,247 @@
       resources_per_trial:
         cpu: 1
         gpu: 1
-  id: hpo
   inputs:
     conf_in: xgboost_model.conf_out
-    test_norm@df_in: split_data.test
-    train_norm@df_in: split_data.train
+    train_norm@df_in: persist.split_data@train
+    test_norm@df_in: persist.split_data@test
   module: greenflow_gquant_plugin.ml
-  type: GridRandomSearchNode
-- conf:
+- id: stock_data
+  type: CsvStockLoader
+  conf:
     file: notebooks/data/stock_price_hist.csv.gz
     path: notebooks/many-small
-  id: stock_data
   inputs: {}
   module: greenflow_gquant_plugin.dataloader
-  type: CsvStockLoader
-- conf:
+- id: stock_feature
+  type: CompositeNode
+  conf:
     input:
-    - preprocess.sort_node@in
+      - preprocess.sort_node@in
     output:
-    - technical_indicator.stock_out
+      - technical_indicator.stock_out
     subnode_ids:
-    - technical_indicator
+      - technical_indicator
     subnodes_conf:
       technical_indicator:
         conf:
           indicators:
-          - args:
-            - 10
-            columns:
-            - close
-            function: port_bollinger_bands
-          - args:
-            - 2
-            - 3
-            columns:
-            - high
-            - low
-            - close
-            - volume
-            function: port_chaikin_oscillator
-          - args:
-            - 2
-            - 3
-            columns:
-            - close
-            function: port_macd
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            function: port_relative_strength_index
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_average_true_range
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_stochastic_oscillator_k
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_stochastic_oscillator_d
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            - volume
-            function: port_money_flow_index
-          - args:
-            - 2
-            columns:
-            - close
-            - volume
-            function: port_force_index
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_ultimate_oscillator
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            - volume
-            function: port_accumulation_distribution
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_commodity_channel_index
-          - args:
-            - 2
-            columns:
-            - close
-            - volume
-            function: port_on_balance_volume
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_vortex_indicator
-          - args:
-            - 3
-            - 4
-            - 5
-            - 6
-            - 7
-            - 8
-            - 9
-            - 10
-            columns:
-            - close
-            function: port_kst_oscillator
-          - args:
-            - 2
-            - 3
-            columns:
-            - high
-            - low
-            function: port_mass_index
-          - args:
-            - 2
-            - 3
-            columns:
-            - close
-            function: port_true_strength_index
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - volume
-            function: port_ease_of_movement
-          - args:
-            - 2
-            columns:
-            - close
-            function: port_coppock_curve
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_keltner_channel
-          - args:
-            - 2
-            columns:
-            - high
-            - low
-            - close
-            function: port_ppsr
-          - args:
-            - 0.9
-            columns:
-            - close
-            function: port_fractional_diff
-          - args:
-            - 0.7
-            columns:
-            - close
-            function: port_fractional_diff
-          - args:
-            - 0.5
-            columns:
-            - close
-            function: port_fractional_diff
-          - args:
-            - 0.3
-            columns:
-            - close
-            function: port_fractional_diff
-          - args:
-            - 0.1
-            columns:
-            - close
-            function: port_fractional_diff
-          - args:
-            - -1
-            columns:
-            - returns
-            function: port_shift
+            - args:
+                - 10
+              columns:
+                - close
+              function: port_bollinger_bands
+            - args:
+                - 2
+                - 3
+              columns:
+                - high
+                - low
+                - close
+                - volume
+              function: port_chaikin_oscillator
+            - args:
+                - 2
+                - 3
+              columns:
+                - close
+              function: port_macd
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+              function: port_relative_strength_index
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_average_true_range
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_stochastic_oscillator_k
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_stochastic_oscillator_d
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+                - volume
+              function: port_money_flow_index
+            - args:
+                - 2
+              columns:
+                - close
+                - volume
+              function: port_force_index
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_ultimate_oscillator
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+                - volume
+              function: port_accumulation_distribution
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_commodity_channel_index
+            - args:
+                - 2
+              columns:
+                - close
+                - volume
+              function: port_on_balance_volume
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_vortex_indicator
+            - args:
+                - 3
+                - 4
+                - 5
+                - 6
+                - 7
+                - 8
+                - 9
+                - 10
+              columns:
+                - close
+              function: port_kst_oscillator
+            - args:
+                - 2
+                - 3
+              columns:
+                - high
+                - low
+              function: port_mass_index
+            - args:
+                - 2
+                - 3
+              columns:
+                - close
+              function: port_true_strength_index
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - volume
+              function: port_ease_of_movement
+            - args:
+                - 2
+              columns:
+                - close
+              function: port_coppock_curve
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_keltner_channel
+            - args:
+                - 2
+              columns:
+                - high
+                - low
+                - close
+              function: port_ppsr
+            - args:
+                - 0.9
+              columns:
+                - close
+              function: port_fractional_diff
+            - args:
+                - 0.7
+              columns:
+                - close
+              function: port_fractional_diff
+            - args:
+                - 0.5
+              columns:
+                - close
+              function: port_fractional_diff
+            - args:
+                - 0.3
+              columns:
+                - close
+              function: port_fractional_diff
+            - args:
+                - 0.1
+              columns:
+                - close
+              function: port_fractional_diff
+            - args:
+                - -1
+              columns:
+                - returns
+              function: port_shift
           remove_na: true
     taskgraph: taskgraphs/xgboost_trade.gq.yaml
-  id: stock_feature
   inputs:
     preprocess@sort_node@in: stock_data.cudf_out
-  type: CompositeNode
-- conf:
+- id: pos_neg_return
+  type: AddSignIndicatorNode
+  conf:
     column: SHIFT_-1
     sign: sign
-  id: pos_neg_return
   inputs:
     in: stock_feature.technical_indicator@stock_out
   module: greenflow_gquant_plugin.transform
-  type: AddSignIndicatorNode
-- conf:
+- id: drop_col
+  type: DropNode
+  conf:
     columns:
-    - indicator
-    - datetime
-    - asset
-    - SHIFT_-1
-    - open
-    - high
-    - low
-    - close
-  id: drop_col
+      - indicator
+      - datetime
+      - asset
+      - SHIFT_-1
+      - open
+      - high
+      - low
+      - close
   inputs:
     in: pos_neg_return.out
   module: greenflow_gquant_plugin.transform
-  type: DropNode
-- conf:
+- id: split_data
+  type: DataSplittingNode
+  conf:
     target: sign
     train_size: 0.8
-  id: split_data
   inputs:
     in: drop_col.out
   module: greenflow_gquant_plugin.ml
-  type: DataSplittingNode
+- id: persist
+  type: PersistNode
+  conf: {}
+  inputs:
+    split_data@test: split_data.test
+    split_data@train: split_data.train
+  module: greenflow_dask_plugin
diff --git a/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_trade.gq.yaml b/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_trade.gq.yaml
index 3e7ee46a..6a9a29de 100644
--- a/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_trade.gq.yaml
+++ b/gQuant/plugins/gquant_plugin/taskgraphs/xgboost_trade.gq.yaml
@@ -1,165 +1,222 @@
-- conf:
+- id: stock_data
+  type: CsvStockLoader
+  conf:
     file: notebooks/data/stock_price_hist.csv.gz
     path: notebooks/many-small
-  id: stock_data
   inputs: {}
   module: greenflow_gquant_plugin.dataloader
-  type: CsvStockLoader
-- conf:
+- id: preprocess
+  type: CompositeNode
+  conf:
     input:
-    - sort_node.in
+      - sort_node.in
     output:
-    - drop_columns.out
+      - drop_columns.out
     subnode_ids:
-    - value_filter
-    - drop_columns
+      - value_filter
+      - drop_columns
     subnodes_conf:
       drop_columns:
         conf:
           columns:
-          - average_volume
-          - min_return
-          - max_return
+            - average_volume
+            - min_return
+            - max_return
       value_filter:
         conf:
-        - column: min_return
-          min: -10
-        - column: max_return
-          max: 10
-        - column: average_volume
-          min: 400
+          - column: min_return
+            min: -10
+          - column: max_return
+            max: 10
+          - column: average_volume
+            min: 400
     taskgraph: taskgraphs/preprocess.gq.yaml
-  id: preprocess
   inputs:
     sort_node@in: stock_data.cudf_out
-  type: CompositeNode
-- conf:
+- id: sort_after
+  type: SortNode
+  conf:
     keys:
-    - asset
-    - datetime
-  id: sort_after
+      - asset
+      - datetime
   inputs:
     in: preprocess.drop_columns@out
   module: greenflow_gquant_plugin.transform
-  type: SortNode
-- conf: {}
-  id: backtest
+- id: train_backtest
+  type: SimpleBackTestNode
+  conf: {}
   inputs:
-    bardata_in: xgboost.stock_out
+    bardata_in: train_signal.stock_out
   module: greenflow_gquant_plugin.backtest
-  type: SimpleBackTestNode
-- conf: {}
-  id: portfolio_opt_train
+- id: portfolio_opt_train
+  type: SimpleAveragePortOpt
+  conf: {}
   inputs:
-    stock_in: train_df.stock_out
+    stock_in: train_backtest.backtest_out
   module: greenflow_gquant_plugin.portofolio
-  type: SimpleAveragePortOpt
-- conf: {}
-  id: sharpe_ratio_trn
+- id: sharpe_ratio_trn
+  type: SharpeRatioNode
+  conf: {}
   inputs:
     stock_in: portfolio_opt_train.stock_out
   module: greenflow_gquant_plugin.analysis
-  type: SharpeRatioNode
-- conf:
+- id: cumulative_return_trn
+  type: CumReturnNode
+  conf:
     label: training cumulative return
     points: 300
-  id: cumulative_return_trn
   inputs:
     in: portfolio_opt_train.stock_out
   module: greenflow_gquant_plugin.analysis
-  type: CumReturnNode
-- conf: {}
-  id: ''
+- id: ''
+  type: Output_Collector
+  conf: {}
   inputs:
     in1: sharpe_ratio_trn.sharpe_out
     in2: cumulative_return_trn.cum_return
     in3: sharpe_ratio_val.sharpe_out
     in4: cumulative_return_val.cum_return
-  type: Output_Collector
-- conf:
+- id: technical_indicator
+  type: IndicatorNode
+  conf:
     indicators:
-    - args:
-      - 10
-      - 20
-      columns:
-      - high
-      - low
-      - close
-      - volume
-      function: port_chaikin_oscillator
-    - args:
-      - 10
-      columns:
-      - close
-      function: port_bollinger_bands
-    - args:
-      - -1
-      columns:
-      - returns
-      function: port_shift
+      - args:
+          - 10
+          - 20
+        columns:
+          - high
+          - low
+          - close
+          - volume
+        function: port_chaikin_oscillator
+      - args:
+          - 10
+        columns:
+          - close
+        function: port_bollinger_bands
+      - args:
+          - -1
+        columns:
+          - returns
+        function: port_shift
     remove_na: true
-  id: technical_indicator
   inputs:
     stock_in: sort_after.out
   module: greenflow_gquant_plugin.transform
-  type: IndicatorNode
-- conf:
-    no_feature:
-    - asset
-    - datetime
-    - volume
-    - close
-    - high
-    - low
-    - returns
-    - indicator
-    - open
-    num_of_rounds: 100
-    target: SHIFT_-1
-    train_date: 2010-01-01
-    xgboost_parameters:
-      gamma: 0
-      max_depth: 8
-      max_leaves: 256
-      objective: reg:squarederror
-  id: xgboost
+- id: train_df
+  type: DatetimeFilterNode
+  conf:
+    beg: '1985-01-01T00:00:00.000Z'
+    end: '2010-01-01T00:00:00.000Z'
   inputs:
     stock_in: technical_indicator.stock_out
-  module: greenflow_gquant_plugin.strategy
-  type: XGBoostStrategyNode
-- conf:
-    beg: 1985-01-01
-    end: 2010-01-01
-  id: train_df
-  inputs:
-    stock_in: backtest.backtest_out
   module: greenflow_gquant_plugin.transform
+- id: validation_df
   type: DatetimeFilterNode
-- conf:
-    beg: 2010-01-01
-    end: 2025-01-01
-  id: validation_df
+  conf:
+    beg: '2010-01-01T00:00:00.000Z'
+    end: '2025-01-01T00:00:00.000Z'
   inputs:
-    stock_in: backtest.backtest_out
+    stock_in: technical_indicator.stock_out
   module: greenflow_gquant_plugin.transform
-  type: DatetimeFilterNode
-- conf: {}
-  id: portfolio_opt_validation
+- id: portfolio_opt_validation
+  type: SimpleAveragePortOpt
+  conf: {}
   inputs:
-    stock_in: validation_df.stock_out
+    stock_in: val_backtest.backtest_out
   module: greenflow_gquant_plugin.portofolio
-  type: SimpleAveragePortOpt
-- conf: {}
-  id: sharpe_ratio_val
+- id: sharpe_ratio_val
+  type: SharpeRatioNode
+  conf: {}
   inputs:
     stock_in: portfolio_opt_validation.stock_out
   module: greenflow_gquant_plugin.analysis
-  type: SharpeRatioNode
-- conf:
+- id: cumulative_return_val
+  type: CumReturnNode
+  conf:
     label: validation cumulative return
     points: 300
-  id: cumulative_return_val
   inputs:
     in: portfolio_opt_validation.stock_out
   module: greenflow_gquant_plugin.analysis
-  type: CumReturnNode
+- id: xboost_model
+  type: TrainXGBoostNode
+  conf:
+    num_of_rounds: 100
+    columns:
+      - datetime
+      - open
+      - close
+      - high
+      - low
+      - asset
+      - volume
+      - returns
+      - indicator
+      - SHIFT_-1
+    include: false
+    xgboost_parameters:
+      eta: 0.3
+      min_child_weight: 1
+      subsample: 1
+      sampling_method: uniform
+      colsample_bytree: 1
+      colsample_bylevel: 1
+      colsample_bynode: 1
+      max_depth: 8
+      max_leaves: 256
+      grow_policy: depthwise
+      gamma: 0
+      lambda: 1
+      alpha: 0
+      tree_method: gpu_hist
+      single_precision_histogram: false
+      deterministic_histogram: false
+      objective: 'reg:squarederror'
+    target: SHIFT_-1
+  inputs:
+    in: persist.train_df@stock_out
+  module: greenflow_gquant_plugin.ml
+- id: train_infer
+  type: InferXGBoostNode
+  conf:
+    prediction: predict
+    pred_contribs: false
+  inputs:
+    model_in: xboost_model.model_out
+    data_in: persist.train_df@stock_out
+  module: greenflow_gquant_plugin.ml
+- id: val_infer
+  type: InferXGBoostNode
+  conf:
+    prediction: predict
+    pred_contribs: false
+  inputs:
+    model_in: xboost_model.model_out
+    data_in: persist.validation_df@stock_out
+  module: greenflow_gquant_plugin.ml
+- id: train_signal
+  type: XGBoostStrategyNode
+  conf: {}
+  inputs:
+    stock_in: train_infer.out
+  module: greenflow_gquant_plugin.strategy
+- id: val_signal
+  type: XGBoostStrategyNode
+  conf: {}
+  inputs:
+    stock_in: val_infer.out
+  module: greenflow_gquant_plugin.strategy
+- id: val_backtest
+  type: SimpleBackTestNode
+  conf: {}
+  inputs:
+    bardata_in: val_signal.stock_out
+  module: greenflow_gquant_plugin.backtest
+- id: persist
+  type: PersistNode
+  conf: {}
+  inputs:
+    train_df@stock_out: train_df.stock_out
+    validation_df@stock_out: validation_df.stock_out
+  module: greenflow_dask_plugin
diff --git a/gQuant/plugins/nemo_plugin/README.md b/gQuant/plugins/nemo_plugin/README.md
index 0f3de14b..98fd928a 100644
--- a/gQuant/plugins/nemo_plugin/README.md
+++ b/gQuant/plugins/nemo_plugin/README.md
@@ -7,20 +7,10 @@ This is an example to show how to write an external greenflow plugin. greenflow
 conda create -n test python=3.8
 ```
 
-### Install the greenflow 
-To install the greenflow graph computation library, run:
-```bash
-pip install greenflow
-```
-Or install `greenflow` at the greenflow directory:
-```bash
-pip install .
-```
-
 ### Install the greenflowlab JupyterLab plugin
 To install `greenflowlab` JupyterLab plugin, make sure `nodejs` of version [12^14^15] is installed. E.g:
 ```bash
-conda install -c conda-forge nodejs=12.4.0
+conda install -c conda-forge python-graphviz nodejs=12.4.0 pydot
 ```
 Then install the `greenflowlab`:
 ```bash
diff --git a/gQuant/plugins/nemo_plugin/greenflow_nemo_plugin/nemoBaseNode.py b/gQuant/plugins/nemo_plugin/greenflow_nemo_plugin/nemoBaseNode.py
index 93ee5c81..aeca954e 100644
--- a/gQuant/plugins/nemo_plugin/greenflow_nemo_plugin/nemoBaseNode.py
+++ b/gQuant/plugins/nemo_plugin/greenflow_nemo_plugin/nemoBaseNode.py
@@ -1,5 +1,5 @@
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema, PortsSpecSchema
-from greenflow.dataframe_flow.portsSpecSchema import NodePorts, MetaData
+from greenflow.dataframe_flow import (
+    ConfSchema, PortsSpecSchema, NodePorts, MetaData)
 
 from nemo.core.neural_types import NmTensor
 import inspect
diff --git a/gQuant/plugins/nemo_plugin/greenflow_nemo_plugin/nemo_util/inferNemo.py b/gQuant/plugins/nemo_plugin/greenflow_nemo_plugin/nemo_util/inferNemo.py
index bc581a7b..a402c0e5 100644
--- a/gQuant/plugins/nemo_plugin/greenflow_nemo_plugin/nemo_util/inferNemo.py
+++ b/gQuant/plugins/nemo_plugin/greenflow_nemo_plugin/nemo_util/inferNemo.py
@@ -1,8 +1,8 @@
 from pathlib import Path
 
 from greenflow.dataframe_flow import Node
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema, PortsSpecSchema
-from greenflow.dataframe_flow.portsSpecSchema import NodePorts, MetaData
+from greenflow.dataframe_flow import (
+    ConfSchema, PortsSpecSchema, NodePorts, MetaData)
 
 from nemo.core.neural_types import NmTensor
 import nemo
diff --git a/gQuant/plugins/nemo_plugin/greenflow_nemo_plugin/nemo_util/trainNemo.py b/gQuant/plugins/nemo_plugin/greenflow_nemo_plugin/nemo_util/trainNemo.py
index 0468a6d6..7d62d7ab 100644
--- a/gQuant/plugins/nemo_plugin/greenflow_nemo_plugin/nemo_util/trainNemo.py
+++ b/gQuant/plugins/nemo_plugin/greenflow_nemo_plugin/nemo_util/trainNemo.py
@@ -1,6 +1,6 @@
 from greenflow.dataframe_flow import Node
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema, PortsSpecSchema
-from greenflow.dataframe_flow.portsSpecSchema import NodePorts,  MetaData
+from greenflow.dataframe_flow import (
+    ConfSchema, PortsSpecSchema, NodePorts,  MetaData)
 
 from nemo.core.neural_types import NmTensor
 import nemo
diff --git a/gQuant/plugins/simple_example/README.md b/gQuant/plugins/simple_example/README.md
index 4352feb3..e1a2d67a 100644
--- a/gQuant/plugins/simple_example/README.md
+++ b/gQuant/plugins/simple_example/README.md
@@ -7,20 +7,10 @@ This is a simple example to show how to write an external greenflow plugin. gree
 conda create -n test python=3.8
 ```
 
-### Install the greenflow 
-To install the greenflow graph computation library, run:
-```bash
-pip install greenflow
-```
-Or install `greenflow` at the greenflow directory:
-```bash
-pip install .
-```
-
 ### Install the greenflowlab JupyterLab plugin
 To install `greenflowlab` JupyterLab plugin, make sure `nodejs` of version [12^14^15] is installed. E.g:
 ```bash
-conda install -c conda-forge nodejs=12.4.0
+conda install -c conda-forge python-graphviz nodejs=12.4.0 pydot
 ```
 Then install the `greenflowlab`:
 ```bash
diff --git a/gQuant/plugins/simple_example/example/__init__.py b/gQuant/plugins/simple_example/example/__init__.py
index e1f4c0f3..d0e4e77a 100644
--- a/gQuant/plugins/simple_example/example/__init__.py
+++ b/gQuant/plugins/simple_example/example/__init__.py
@@ -74,4 +74,4 @@ def copy_df(df_obj):
 
 
 register_validator(pd.DataFrame, _validate_df)
-register_copy_function(pd.DataFrame, copy_df)
\ No newline at end of file
+register_copy_function(pd.DataFrame, copy_df)
diff --git a/gQuant/plugins/simple_example/example/distanceNode.py b/gQuant/plugins/simple_example/example/distanceNode.py
index fd39d804..29519824 100644
--- a/gQuant/plugins/simple_example/example/distanceNode.py
+++ b/gQuant/plugins/simple_example/example/distanceNode.py
@@ -1,82 +1,77 @@
-import pandas as pd
 import numpy as np
-from greenflow.dataframe_flow import Node, MetaData
-from greenflow.dataframe_flow import NodePorts, PortsSpecSchema
+from greenflow.dataframe_flow import Node
+from greenflow.dataframe_flow import PortsSpecSchema
 from greenflow.dataframe_flow import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
 
 
-class DistanceNode(Node):
+class DistanceNode(TemplateNodeMixin, Node):
 
-    def ports_setup(self):
+    def init(self):
+        TemplateNodeMixin.init(self)
+        self.delayed_process = True
         port_type = PortsSpecSchema.port_type
-        input_ports = {
-            'points_df_in': {
-                port_type: [pd.DataFrame]
+        self.INPUT_PORT_NAME = "points_df_in"
+        self.OUTPUT_PORT_NAME = "distance_df"
+        self.ABS_OUTPUT_PORT_NAME = "distance_abs_df"
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: ["pandas.DataFrame"]
             }
         }
-
-        output_ports = {
-            'distance_df': {
-                port_type: [pd.DataFrame]
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:points_df_in}"
+            },
+            self.ABS_OUTPUT_PORT_NAME: {
+                port_type: "${port:points_df_in}"
             },
-            'distance_abs_df': {
-                PortsSpecSchema.port_type:  [pd.DataFrame]
-            }
         }
-        input_connections = self.get_connected_inports()
-        if 'points_df_in' in input_connections:
-            types = input_connections['points_df_in']
-            # connected, use the types passed in from parent
-            return NodePorts(inports={'points_df_in': {port_type: types}},
-                             outports={'distance_df': {port_type: types},
-                                       'distance_abs_df': {port_type: types},
-                                       })
-        else:
-            return NodePorts(inports=input_ports, outports=output_ports)
-
-    def conf_schema(self):
-        return ConfSchema()
-
-    def init(self):
-        self.delayed_process = True
-
-    def meta_setup(self):
         req_cols = {
             'x': 'float64',
             'y': 'float64'
         }
-        required = {
-            'points_df_in': req_cols,
+        meta_inports = {
+            self.INPUT_PORT_NAME: req_cols
         }
-        input_meta = self.get_input_meta()
-        output_cols = ({
-                'distance_df': {
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: {
                     'distance_cudf': 'float64',
-                    'x': 'float64',
-                    'y': 'float64'
-                },
-                'distance_abs_df': {
+                }
+            },
+            self.ABS_OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: {
                     'distance_abs_cudf': 'float64',
-                    'x': 'float64',
-                    'y': 'float64'
                 }
-            })
-        if 'points_df_in' in input_meta:
-            col_from_inport = input_meta['points_df_in']
-            # additional ports
-            output_cols['distance_df'].update(col_from_inport)
-            output_cols['distance_abs_df'].update(col_from_inport)
-        return MetaData(inports=required, outports=output_cols)
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
+
+    def conf_schema(self):
+        return ConfSchema()
 
     def process(self, inputs):
-        df = inputs['points_df_in']
+        df = inputs[self.INPUT_PORT_NAME]
         output = {}
-        if self.outport_connected('distance_df'):
+        if self.outport_connected(self.OUTPUT_PORT_NAME):
             copy_df = df.copy()
             copy_df['distance_cudf'] = np.sqrt((df['x'] ** 2 + df['y'] ** 2))
-            output.update({'distance_df': copy_df})
-        if self.outport_connected('distance_abs_df'):
+            output.update({self.OUTPUT_PORT_NAME: copy_df})
+        if self.outport_connected(self.ABS_OUTPUT_PORT_NAME):
             copy_df = df.copy()
             copy_df['distance_abs_cudf'] = np.abs(df['x']) + np.abs(df['y'])
-            output.update({'distance_abs_df': copy_df})
+            output.update({self.ABS_OUTPUT_PORT_NAME: copy_df})
         return output
diff --git a/gQuant/plugins/simple_example/example/pointNode.py b/gQuant/plugins/simple_example/example/pointNode.py
index 9a244403..8896cdea 100644
--- a/gQuant/plugins/simple_example/example/pointNode.py
+++ b/gQuant/plugins/simple_example/example/pointNode.py
@@ -1,20 +1,41 @@
 import numpy as np
 import pandas as pd
-from greenflow.dataframe_flow import Node, MetaData
-from greenflow.dataframe_flow import NodePorts, PortsSpecSchema
+from greenflow.dataframe_flow import Node
+from greenflow.dataframe_flow import PortsSpecSchema
 from greenflow.dataframe_flow import ConfSchema
+from greenflow.dataframe_flow.metaSpec import MetaDataSchema
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
 
 
-class PointNode(Node):
+class PointNode(TemplateNodeMixin, Node):
 
-    def ports_setup(self):
-        input_ports = {}
-        output_ports = {
-            'points_df_out': {
-                PortsSpecSchema.port_type: pd.DataFrame
+    def init(self):
+        TemplateNodeMixin.init(self)
+        self.OUTPUT_PORT_NAME = 'points_df_out'
+        port_inports = {}
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                PortsSpecSchema.port_type: ["pandas.DataFrame"]
             }
         }
-        return NodePorts(inports=input_ports, outports=output_ports)
+        meta_inports = {}
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: {
+                    'x': 'float64',
+                    'y': 'float64'
+                }
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
@@ -35,24 +56,12 @@ def conf_schema(self):
         }
         return ConfSchema(json=json, ui=ui)
 
-    def init(self):
-        pass
-
-    def meta_setup(self):
-        columns_out = {
-            'points_df_out': {
-                'x': 'float64',
-                'y': 'float64'
-            },
-        }
-        return MetaData(inports={}, outports=columns_out)
-
     def process(self, inputs):
         npts = self.conf['npts']
         df = pd.DataFrame()
         df['x'] = np.random.rand(npts)
         df['y'] = np.random.rand(npts)
         output = {}
-        if self.outport_connected('points_df_out'):
-            output.update({'points_df_out': df})
+        if self.outport_connected(self.OUTPUT_PORT_NAME):
+            output.update({self.OUTPUT_PORT_NAME: df})
         return output
diff --git a/greenflow/greenflow/__init__.py b/greenflow/greenflow/__init__.py
index e69de29b..c1c34a7e 100644
--- a/greenflow/greenflow/__init__.py
+++ b/greenflow/greenflow/__init__.py
@@ -0,0 +1,5 @@
+from .dataframe_flow import TaskGraph
+from .dataframe_flow.node import Node
+from .dataframe_flow import PortsSpecSchema
+
+__all__ = ["TaskGraph", "Node", "PortsSpecSchema"]
diff --git a/greenflow/greenflow/dataframe_flow/__init__.py b/greenflow/greenflow/dataframe_flow/__init__.py
index 1476005d..cf045db3 100644
--- a/greenflow/greenflow/dataframe_flow/__init__.py
+++ b/greenflow/greenflow/dataframe_flow/__init__.py
@@ -2,6 +2,7 @@
 from .taskSpecSchema import *  # noqa: F401,F403
 from .taskGraph import *  # noqa: F401,F403
 from .portsSpecSchema import *  # noqa: F401,F403
+from .metaSpec import *  # noqa: F401,F403
 import sys
 try:
     # For python 3.8 and later
@@ -11,7 +12,8 @@
     import importlib_metadata
 
 # load all the plugins from entry points
-for entry_point in importlib_metadata.entry_points().get('greenflow.plugin', ()):
+for entry_point in \
+        importlib_metadata.entry_points().get('greenflow.plugin', ()):
     mod = entry_point.load()
     name = entry_point.name
     sys.modules[name] = mod
diff --git a/greenflow/greenflow/dataframe_flow/_node_extension_mixin.py b/greenflow/greenflow/dataframe_flow/_node_extension_mixin.py
new file mode 100644
index 00000000..d14a3728
--- /dev/null
+++ b/greenflow/greenflow/dataframe_flow/_node_extension_mixin.py
@@ -0,0 +1,275 @@
+import re
+import importlib
+from .portsSpecSchema import (PortsSpecSchema, NodePorts)
+from .metaSpec import (MetaDataSchema, MetaData)
+
+
+__all__ = ['NodeExtensionMixin']
+
+TYPES_CACHE = {}
+
+
+class NodeExtensionMixin:
+    def _sep_variable(self, variable):
+        assert isinstance(variable, str)
+        e = re.search('^\${(.*):(.*)}$', variable) # noqa
+        if e is None and variable.startswith('$'):
+            raise ValueError("varaible format is wrong")
+        if e is None:
+            return None
+        groups = e.groups()
+        return groups
+
+    def _parse_variable(self, variable, port_inports):
+        if isinstance(variable, int):
+            return variable
+
+        if isinstance(variable, dict):
+            return variable
+
+        if variable is None:
+            return None
+
+        port_type = PortsSpecSchema.port_type
+        groups = self._sep_variable(variable)
+        if groups is None:
+            return variable
+
+        if groups[0] == 'conf':
+            return self.conf[groups[1]]
+        elif groups[0] == 'port':
+            return port_inports[groups[1]][port_type]
+        else:
+            raise KeyError('Cannot parse variable {}'.format(groups))
+
+    def _load_type(self, type_str):
+        return_list = False
+        if isinstance(type_str, list):
+            return_list = True
+            type_str_list = type_str
+        else:
+            type_str_list = [type_str]
+
+        clsobj_list = []
+        for type_str in type_str_list:
+            if type_str in TYPES_CACHE:
+                clsobj_list.append(TYPES_CACHE[type_str])
+
+            if isinstance(type_str, type):
+                clsobj = type_str
+            elif isinstance(type_str, str):
+                splits = type_str.split('.')
+                mod_str = ".".join(splits[:-1])
+                mod = importlib.import_module(mod_str)
+                clsobj = getattr(mod, splits[-1])
+                TYPES_CACHE[type_str] = clsobj
+            else:
+                raise Exception('Cannot load type: {}'.format(type_str))
+
+            clsobj_list.append(clsobj)
+
+        if return_list:
+            return clsobj_list
+        else:
+            return clsobj_list[0]
+
+    def _resolve_ports(self, ports_template):
+        '''The ports can be defined via template specification.
+
+        Example:
+            port_inports = {
+                "port0_name": {
+                    PortsSpecSchema.port_type: ["type0", "type1"]
+                },
+                "port1_name": {
+                    PortsSpecSchema.port_type: "${conf:some_type}",
+                    PortsSpecSchema.dynamic: {
+                        # choie can be True/False, list of types or string
+                        # True, generate outports matching the
+                        # connected dynamic input ports, use the
+                        # the same type as the dynamic port
+                        # False, not generate matching outports
+                        # list of types or string, same as True condition,
+                        # but use the specified types
+                        PortsSpecSchema.DYN_MATCH: ['type0', 'type1']
+                    }
+                },
+                ...
+            }
+
+            port_outports = {
+                "port0_name": {
+                    PortsSpecSchema.port_type: ["type0", "type1"]
+                },
+                "port1_name": {
+                    PortsSpecSchema.port_type: "${port:port0_name}"
+                },
+                ...
+            }
+
+
+            ports_template = NodePorts(inports=port_inports,
+                                       outports=port_outports)
+            ports_resolved = self._resolve_ports(ports_template)
+
+        Above, the types are specified as strings and loaded dynamically.
+        Additionally an input port can use "dynamic" syntax for automatically
+        resolving types for the input connections to that port. The output
+        ports can similarlly define types as string to be loaded dynamically,
+        and make references to port inputs to re-use an input port's types.
+        After calling _resolve_ports the ports definitions would look something
+        as follows:
+            ports_resolved.inports == {
+                "port0_name": {
+                    PortsSpecSchema.port_type: [type0, type1]
+                },
+                "port1_name": {
+                    PortsSpecSchema.port_type: "${conf:some_type}",
+                    PortsSpecSchema.dynamic: {
+                        PortsSpecSchema.DYN_MATCH: [type0, type1]
+                    }
+                },
+                ...
+            }
+
+            ports_resolved.inports == {
+                "port0_name": {
+                    PortsSpecSchema.port_type: [type0, type1]
+                },
+                "port1_name": {
+                    PortsSpecSchema.port_type: "${port:port0_name}"
+                },
+                ...
+            }
+
+        Port types using "$" syntax are resolved when the node is within a
+        taskgraph context. This additional resolve logic is handled in
+        :class:`NodeTaskGraphExtensionMixin.port_setup_ext`.
+
+        :param ports_template: Ports definition via convenience templating.
+        :type ports_template: NodePorts
+
+        :returns: Resolved ports.
+        :rtype: NodePorts
+
+        '''
+        ports = ports_template
+        dy = PortsSpecSchema.dynamic
+        port_type = PortsSpecSchema.port_type
+        # resolve all the variables
+        port_inports = {}
+        inports = ports.inports
+        for key in inports:
+            key_name = self._parse_variable(key, inports)
+            value = inports[key]
+            ptype = value[port_type]
+            return_list = False
+            if isinstance(ptype, list):
+                return_list = True
+                ptype_list = ptype
+            else:
+                ptype_list = [ptype]
+
+            loaded_types = [
+                self._load_type(self._parse_variable(item, inports))
+                if not isinstance(item, type) else item
+                for item in ptype_list
+            ]
+
+            if return_list:
+                value[port_type] = loaded_types
+            else:
+                value[port_type] = loaded_types[0]
+
+            if dy in value:
+                dynamic_value = value[dy]
+                m_outputs = dynamic_value[PortsSpecSchema.DYN_MATCH]
+                if isinstance(m_outputs, bool):
+                    pass
+                elif isinstance(m_outputs, list):
+                    dynamic_value[PortsSpecSchema.DYN_MATCH] = [
+                        self._load_type(self._parse_variable(item, inports))
+                        if not isinstance(item, type) else item
+                        for item in m_outputs
+                    ]
+                elif isinstance(m_outputs, str):
+                    dynamic_value[PortsSpecSchema.DYN_MATCH] = self._load_type(
+                        self._parse_variable(m_outputs, inports))
+                else:
+                    raise ValueError
+
+            port_inports[key_name] = value
+
+        # resolve all the variables
+        port_outports = {}
+        outports = ports.outports
+        for key in outports:
+            key_name = self._parse_variable(key, port_inports)
+            value = outports[key]
+            if isinstance(value[port_type], list):
+                value[port_type] = [
+                    self._load_type(self._parse_variable(item, port_inports))
+                    if not isinstance(item, type) else item
+                    for item in value[port_type]
+                ]
+            elif isinstance(value[port_type], str):
+                # This part is valid if node is part of NodeTaskGraphMixin
+                if not value[port_type].startswith('$'):
+                    value[port_type] = self._load_type(
+                        self._parse_variable(value[port_type],
+                                             port_inports))
+                else:
+                    # it will be resolved inside the port_setup_ext
+                    pass
+            elif isinstance(value[port_type], type):
+                pass
+            else:
+                raise ValueError
+
+            port_outports[key_name] = value
+
+        return NodePorts(inports=port_inports, outports=port_outports)
+
+    def _resolve_meta(self, meta_template, port_inports):
+        meta = meta_template
+        meta_inports = {}
+        metainports = meta.inports
+
+        for key in metainports:
+            key_name = self._parse_variable(key, port_inports)
+            value = metainports[key]
+
+            new_value = {}
+            for vk in value:
+                nvk = self._parse_variable(vk, port_inports)
+                new_value[nvk] = self._parse_variable(value[vk],
+                                                      port_inports)
+            meta_inports[key_name] = new_value
+
+        meta_outports = {}
+        metaoutports = meta.outports
+        data_accessor = MetaDataSchema.META_DATA
+        order_accessor = MetaDataSchema.META_ORDER
+        for key in metaoutports:
+            meta_outports[key] = metaoutports[key].copy()
+
+            key_name = self._parse_variable(key, port_inports)
+            value = metaoutports[key]
+
+            if data_accessor in value:
+                new_data = {}
+                for vk in value[data_accessor]:
+                    nvk = self._parse_variable(vk, port_inports)
+                    new_data[nvk] = self._parse_variable(
+                        value[data_accessor][vk], port_inports)
+
+                meta_outports[key_name][data_accessor] = new_data
+
+            if order_accessor in value:
+                new_order = {}
+                for vk in value[order_accessor]:
+                    nvk = self._parse_variable(vk, port_inports)
+                    new_order[nvk] = value[order_accessor][vk]
+                meta_outports[key_name][order_accessor] = new_order
+
+        return MetaData(inports=meta_inports, outports=meta_outports)
diff --git a/greenflow/greenflow/dataframe_flow/_node_flow.py b/greenflow/greenflow/dataframe_flow/_node_flow.py
index d4586f00..6fda95f4 100644
--- a/greenflow/greenflow/dataframe_flow/_node_flow.py
+++ b/greenflow/greenflow/dataframe_flow/_node_flow.py
@@ -9,7 +9,9 @@
 
 from .portsSpecSchema import PortsSpecSchema
 from .taskSpecSchema import TaskSpecSchema
+from .metaSpec import MetaData
 from ._node import _Node
+from ._node_taskgraph_extension_mixin import NodeTaskGraphExtensionMixin
 
 # OUTPUT_ID = 'f291b900-bd19-11e9-aca3-a81e84f29b0f_uni_output'
 OUTPUT_ID = 'collector_id_fd9567b6'
@@ -49,6 +51,7 @@ def _get_nodetype(node):
         # Using nodet.__name__ != 'Node' to avoid cyclic dependencies.
         if issubclass(nodet, _Node) and \
                 not issubclass(nodet, NodeTaskGraphMixin) and \
+                not issubclass(nodet, NodeTaskGraphExtensionMixin) and \
                 nodet is not _Node and \
                 nodet.__name__ != 'Node':
             keeptypes.append(nodet)
@@ -74,7 +77,7 @@ def register_cleanup(name: str,
     _CLEANUP[name] = fun
 
 
-class NodeTaskGraphMixin(object):
+class NodeTaskGraphMixin(NodeTaskGraphExtensionMixin):
     '''Relies on mixing in with a Node class that has the following attributes
     and methods:
         ATTRIBUTES
@@ -103,6 +106,9 @@ def __getstate__(self):
         # print('state', state)
         return state
 
+    def __getitem__(self, key):
+        return getattr(self, key)
+
     def __setstate__(self, state):
         self.__dict__.update(state)
 
@@ -122,6 +128,58 @@ def __init__(self):
         #     typically a data container.
         self.clear_input = True
 
+    def update(self):
+        """
+        Within a task graph context override a Node's update to cache: ports,
+        metadata, input connections, and input metadata.
+        """
+
+        # this will filter out the primary class with ports_setup
+        nodecls_list = _get_nodetype(self)
+        for icls in nodecls_list:
+            if not hasattr(icls, 'update'):
+                continue
+            # Within update resolve ports and meta via class NodeExtensionMixin
+            # methods _resolve_ports and _resolve_meta.
+            icls.update(self)
+            break
+
+        # cache it after update
+        NodeTaskGraphExtensionMixin.cache_update_result(self)
+
+        # cache the conf_schema too
+        for icls in nodecls_list:
+            if not hasattr(icls, 'conf_schema'):
+                continue
+            self.conf_schema_cache = self.conf_schema()
+            break
+        else:
+            pass
+
+    def conf_schema(self):
+        if hasattr(self, 'conf_schema_cache'):
+            return self.conf_schema_cache
+        nodecls_list = _get_nodetype(self)
+        for icls in nodecls_list:
+            if not hasattr(icls, 'conf_schema'):
+                continue
+            return icls.conf_schema(self)
+
+    def meta_setup(self):
+        if hasattr(self, 'meta_data_cache'):
+            return self.meta_data_cache
+
+        nodecls_list = _get_nodetype(self)
+        meta = MetaData()
+        for icls in nodecls_list:
+            if not hasattr(icls, 'meta_setup'):
+                continue
+
+            meta = icls.meta_setup(self)
+            break
+
+        return meta
+
     def ports_setup(self):
         """
         overwrite the super class ports_setup so it can calculate the dynamic
@@ -130,15 +188,20 @@ def ports_setup(self):
         :return: Node ports
         :rtype: NodePorts
         """
-        # this will filter out the primary class with ports_setup
-        nodecls_list = _get_nodetype(self)
-        for icls in nodecls_list:
-            if not hasattr(icls, 'ports_setup'):
-                continue
-            ports = icls.ports_setup(self)
-            break
+        if hasattr(self, 'ports_setup_cache'):
+            ports = self.ports_setup_cache
         else:
-            raise Exception('ports_setup method missing')
+            # this will filter out the primary class with ports_setup
+            nodecls_list = _get_nodetype(self)
+            for icls in nodecls_list:
+                if not hasattr(icls, 'ports_setup'):
+                    continue
+
+                ports = icls.ports_setup(self)
+                break
+            else:
+                raise Exception('ports_setup method missing')
+
         # note, currently can only handle one dynamic port per node
         port_type = PortsSpecSchema.port_type
         inports = ports.inports
@@ -149,20 +212,36 @@ def ports_setup(self):
                 break
         else:
             return ports
+
         if hasattr(self, 'inputs'):
+            has_dynamic = False
             for inp in self.inputs:
                 to_port = inp['to_port']
-                if to_port in inports and (not inports[to_port].get(dy,
-                                                                    False)):
+                if to_port in inports and not inports[to_port].get(dy, False):
                     # skip connected non dynamic ports
                     continue
                 else:
-                    inports[inp['from_node'].uid+'@'+inp['from_port']] = {
-                        port_type: types, dy: True}
+                    has_dynamic = True
+
+            if has_dynamic:
+                connected_inports = self.get_connected_inports()
+                for inp in self.inputs:
+                    to_port = inp['to_port']
+                    if to_port in inports and (not inports[to_port].get(
+                            dy, False)):
+                        # skip connected non dynamic ports
+                        continue
+                    else:
+                        if to_port in connected_inports:
+                            types = connected_inports[to_port]
+                        inports[inp['from_node'].uid+'@'+inp['from_port']] = {
+                            port_type: types, dy: True}
+
         return ports
 
-    def __valide(self, node_output: dict):
+    def __validate_output(self, node_output: dict):
         output_meta = self.meta_setup().outports
+
         # Validate each port
         out_ports = self._get_output_ports(full_port_spec=True)
         for pname, pspec in out_ports.items():
@@ -231,29 +310,49 @@ def __input_ready(self):
     def __get_input_df(self):
         return self.input_df
 
-    def get_input_meta(self):
+    def get_input_meta(self, port_name=None):
         """
-        get all the connected input metas information
+        if port_name is None, get all the connected input metas information
         returns
             dict, key is the current node input port name, value is the column
             name and types
+        if port_name is not None, get meta data for the input port_name. If it
+        doesn't exist, return None
         """
+        if hasattr(self, 'input_meta_cache'):
+            if port_name is None:
+                return self.input_meta_cache
+            elif port_name in self.input_meta_cache:
+                return self.input_meta_cache[port_name]
+            else:
+                # Run the logic below to find the port
+                # Warning: Something might not be right if this happens.
+                #     Perhaps the cache needs to be reset.
+                pass
+
         output = {}
         if not hasattr(self, 'inputs'):
             return output
-        ports = self.ports_setup()
-        inports = ports.inports
-        dy = PortsSpecSchema.dynamic
+
+        out_port_names = []
+        to_port_names = []
+        from_port_names = []
+        meta_data_list = []
         for node_input in self.inputs:
             from_node = node_input['from_node']
             meta_data = copy.deepcopy(from_node.meta_setup())
+
             from_port_name = node_input['from_port']
             to_port_name = node_input['to_port']
+            if port_name is not None and port_name == to_port_name:
+                return meta_data.outports[from_port_name]
+
             if from_port_name not in meta_data.outports:
                 nodetype_list = _get_nodetype(self)
                 nodetype_names = [inodet.__name__ for inodet in nodetype_list]
                 if 'OutputCollector' in nodetype_names:
                     continue
+
                 warnings.warn(
                     'node "{}" node-type "{}" to port "{}", from node "{}" '
                     'node-type "{}" oport "{}" missing oport in metadata for '
@@ -264,11 +363,28 @@ def get_input_meta(self):
                 )
             else:
                 out_port_name = from_node.uid+'@'+from_port_name
-                if out_port_name in inports and inports[
-                        out_port_name].get(dy, False):
+                out_port_names.append(out_port_name)
+                to_port_names.append(to_port_name)
+                from_port_names.append(from_port_name)
+                meta_data_list.append(meta_data)
+
+        if port_name is not None:
+            return None
+
+        if len(out_port_names) > 0:
+            dy = PortsSpecSchema.dynamic
+            ports = self.ports_setup()
+
+            inports = ports.inports
+            for out_port_name, to_port_name, from_port_name, meta_data in zip(
+                    out_port_names, to_port_names, from_port_names,
+                    meta_data_list):
+                if out_port_name in inports and inports[out_port_name].get(
+                        dy, False):
                     output[out_port_name] = meta_data.outports[from_port_name]
                 else:
                     output[to_port_name] = meta_data.outports[from_port_name]
+
         return output
 
     def __set_input_df(self, to_port, df):
@@ -533,11 +649,15 @@ def get_connected_inports(self):
             type passed from parent
         """
 
+        if hasattr(self, 'input_connections_cache'):
+            return self.input_connections_cache
+
         def get_type(type_def):
             if isinstance(type_def, list):
                 return type_def
             else:
                 return [type_def]
+
         output = {}
         if not hasattr(self, 'inputs'):
             return output
@@ -552,6 +672,7 @@ def get_type(type_def):
                 output[to_port_name] = oport_types
             else:
                 continue
+
         return output
 
     def decorate_process(self):
@@ -590,7 +711,7 @@ def __call__(self, inputs_data):
         if self.uid != OUTPUT_ID and output_df is None:
             raise Exception("None output")
         else:
-            self.__valide(output_df)
+            self.__validate_output(output_df)
 
         if self.save:
             self.save_cache(output_df)
@@ -707,6 +828,9 @@ def validate_required(iport, kcol, kval, ientnode, icols):
 
             ival = icols[kcol]
             if kval != ival:
+                if kval is None:
+                    # bypass None type
+                    return
                 # special case for 'date'
                 if (kval == 'date' and ival
                         in ('datetime64[ms]', 'date', 'datetime64[ns]')):
@@ -722,6 +846,7 @@ def validate_required(iport, kcol, kval, ientnode, icols):
 
         if not required:
             return
+
         inports = self._get_input_ports(full_port_spec=True)
         for iport in inports:
             if iport not in required:
diff --git a/greenflow/greenflow/dataframe_flow/_node_taskgraph_extension_mixin.py b/greenflow/greenflow/dataframe_flow/_node_taskgraph_extension_mixin.py
new file mode 100644
index 00000000..0363df0f
--- /dev/null
+++ b/greenflow/greenflow/dataframe_flow/_node_taskgraph_extension_mixin.py
@@ -0,0 +1,217 @@
+from .portsSpecSchema import (PortsSpecSchema, NodePorts)
+from .metaSpec import (MetaDataSchema, MetaData)
+
+__all__ = ['NodeTaskGraphExtensionMixin']
+
+
+class NodeTaskGraphExtensionMixin:
+    '''Extension logic for a Node within a taskgraph. This mixin is used with
+    NodeTaskGraphMixin.
+    '''
+
+    def reset_cache(self):
+        '''Delete ivars that maintain the cache state for a node.'''
+        if hasattr(self, 'ports_setup_cache'):
+            del self.ports_setup_cache
+
+        if hasattr(self, 'input_meta_cache'):
+            del self.input_meta_cache
+
+        if hasattr(self, 'input_connections_cache'):
+            del self.input_connections_cache
+
+        if hasattr(self, 'meta_data_cache'):
+            del self.meta_data_cache
+
+    def cache_update_result(self):
+        self.reset_cache()
+        # cache all the intermediate results
+        self.ports_setup_cache = self.ports_setup()
+        self.input_meta_cache = self.get_input_meta()
+        self.input_connections_cache = self.get_connected_inports()
+        self.meta_data_cache = self.meta_setup()
+
+    def ports_setup_ext(self, ports):
+        '''
+        1. Finds the port type by the connected node port type.
+        2. Set the port type to the determined type.
+        3. If the node is not connected, it will use the list of the types.
+        4. Also handles the dynamic port type.
+        5. Set the port type to the determined type (from the graph topology)
+           for the dynamic port.
+
+        :param ports: These are resolved ports.
+        :type ports: NodePorts
+
+        :return: Node ports
+        :rtype: NodePorts
+        '''
+        port_inports = ports.inports
+        port_outports = ports.outports
+
+        port_type = PortsSpecSchema.port_type
+        dy = PortsSpecSchema.dynamic
+        input_connections = self.get_connected_inports()
+
+        dynamic = None
+        inports = {}
+        for input_port in port_inports:
+            if input_port in input_connections:
+                determined_type = input_connections[input_port]
+                inports[input_port] = {port_type: determined_type}
+            else:
+                types = port_inports[input_port][port_type]
+                inports[input_port] = {port_type: types}
+
+            if dy in port_inports[input_port]:
+                inports[input_port][dy] = True
+                dynamic = \
+                    port_inports[input_port][dy][PortsSpecSchema.DYN_MATCH]
+
+        outports = {}
+        for output_port in port_outports:
+            types = port_outports[output_port][port_type]
+            if isinstance(types, str) and types.startswith('$'):
+                groups = self._sep_variable(types)
+                if groups[0] != 'port':
+                    raise ValueError("expect variable {} refer to port".format(
+                        groups[0]))
+                if groups[1] not in inports:
+                    raise ValueError(
+                        "expect variable name {} refer to a inport name"
+                        .format(groups[1]))
+                input_port_name = groups[1]
+                outports[output_port] = {
+                    port_type:
+                    inports[input_port_name][port_type]
+                }
+            else:
+                outports[output_port] = {port_type: types}
+
+        static_inport_names = [
+            iport
+            for iport in port_inports if not inports[iport].get(dy, False)
+        ]
+
+        for input_port in port_inports:
+            dynamic = None
+            if dy in port_inports[input_port]:
+                dynamic = \
+                    port_inports[input_port][dy][PortsSpecSchema.DYN_MATCH]
+
+            if dynamic is not None:
+                types = None
+                if not isinstance(dynamic, bool):
+                    types = dynamic
+
+                for port_name in input_connections.keys():
+                    # if port_name not in port_inports:
+                    if port_name not in outports and \
+                            port_name not in static_inport_names:
+                        if types is not None:
+                            outports[port_name] = {port_type: types}
+                        else:
+                            if isinstance(dynamic, bool) and dynamic:
+                                types = input_connections[port_name]
+                                outports[port_name] = {port_type: types}
+
+        return NodePorts(inports=inports, outports=outports)
+
+    def meta_setup_ext(self, meta):
+        '''
+        1. Based on meta operators, calculate the output meta
+        2. Adjust meta data element orders based on specified order
+        3. Pass the meta data for dynamically added output ports
+
+        :param meta: the meta information that needs to be calculated.
+        :type MetaData:  MetaData
+
+        :return: MetaData
+        :rtype: MetaData
+        '''
+        input_meta = self.get_input_meta()
+
+        inports = meta.inports.copy()
+        metaoutports = meta.outports
+        outports = {}
+        data_accessor = MetaDataSchema.META_DATA
+        order_accessor = MetaDataSchema.META_ORDER
+        for out_port_name in metaoutports:
+            type_str = metaoutports[out_port_name].get(MetaDataSchema.META_OP)
+            if type_str is None:
+                # NOT A META_OP
+                outports[out_port_name] = metaoutports[out_port_name]
+            elif type_str == MetaDataSchema.META_OP_ADDITION:
+                input_port = \
+                    metaoutports[out_port_name][MetaDataSchema.META_REF_INPUT]
+                if input_port in input_meta:
+                    input_meta_data = input_meta[input_port]
+                else:
+                    input_meta_data = inports[input_port]
+
+                outports[out_port_name] = input_meta_data.copy()
+                outports[out_port_name].update(
+                    metaoutports[out_port_name][data_accessor])
+            elif type_str == MetaDataSchema.META_OP_RETENTION:
+                outports[out_port_name] = \
+                    metaoutports[out_port_name][data_accessor].copy()
+            elif type_str == MetaDataSchema.META_OP_DELETION:
+                input_port = metaoutports[out_port_name][
+                    MetaDataSchema.META_REF_INPUT]
+                if input_port in input_meta:
+                    input_meta_data = input_meta[input_port]
+                else:
+                    input_meta_data = inports[input_port]
+
+                outports[out_port_name] = input_meta_data.copy()
+                for key in metaoutports[out_port_name][data_accessor]:
+                    if key in outports[out_port_name]:
+                        del outports[out_port_name][key]
+            else:
+                raise NotImplementedError('META_OP "{}" not implemented'
+                                          .format(type_str))
+
+            # adjust the columns order
+            if order_accessor in metaoutports[out_port_name]:
+                total_properties = len(outports[out_port_name])
+                order_dict = metaoutports[out_port_name][order_accessor].copy()
+                key_lists = list(outports[out_port_name].keys())
+                for key in order_dict:
+                    if order_dict[key] < 0:
+                        order_dict[key] += total_properties
+
+                    if key in key_lists:
+                        key_lists.remove(key)
+
+                items = list(order_dict.items())
+                items.sort(key=lambda x: x[1])
+                for i in items:
+                    key_lists.insert(i[1], i[0])
+
+                old_dict = outports[out_port_name]
+                outports[out_port_name] = {
+                    k: old_dict[k]
+                    for k in key_lists if k in old_dict
+                }
+
+        # handle the dynamic output meta_setup
+        dy = PortsSpecSchema.dynamic
+        port_inports = self.ports_setup().inports
+        static_inport_names = [
+            iport
+            for iport in port_inports if not port_inports[iport].get(dy, False)
+        ]
+        for input_port in port_inports:
+            isdynamic = None
+            if dy in port_inports[input_port]:
+                isdynamic = port_inports[input_port][dy]
+
+            if isdynamic is not None and isdynamic:
+                input_connections = self.get_connected_inports()
+                for port_name in input_connections.keys():
+                    if port_name not in metaoutports and \
+                            port_name not in static_inport_names and \
+                            port_name in input_meta:
+                        outports[port_name] = input_meta[port_name]
+
+        return MetaData(inports=inports, outports=outports)
diff --git a/greenflow/greenflow/dataframe_flow/cache.py b/greenflow/greenflow/dataframe_flow/cache.py
deleted file mode 100644
index d9aaaa83..00000000
--- a/greenflow/greenflow/dataframe_flow/cache.py
+++ /dev/null
@@ -1,3 +0,0 @@
-CACHE_PORTS = {}
-CACHE_SCHEMA = {}
-CACHE_META = {}
diff --git a/greenflow/greenflow/dataframe_flow/config_nodes_modules.py b/greenflow/greenflow/dataframe_flow/config_nodes_modules.py
new file mode 100644
index 00000000..03506f9f
--- /dev/null
+++ b/greenflow/greenflow/dataframe_flow/config_nodes_modules.py
@@ -0,0 +1,286 @@
+import os
+import sys
+from pathlib import Path
+import copy
+import pkgutil
+import inspect
+from collections import namedtuple
+import configparser
+import importlib
+
+from .util import get_file_path
+from .taskSpecSchema import TaskSpecSchema
+from .task import Task
+from ._node import _Node
+from ._node_flow import NodeTaskGraphMixin
+
+
+DEFAULT_MODULE = os.getenv('GREENFLOW_PLUGIN_MODULE', "greenflow.plugin_nodes")
+
+MODULE_CACHE = {}
+
+
+class ConfigParser(configparser.ConfigParser):
+    """Can get options() without defaults
+    """
+
+    def options(self, section, no_defaults=True, **kwargs):
+        if no_defaults:
+            try:
+                return list(self._sections[section].keys())
+            except KeyError:
+                raise configparser.NoSectionError(section)
+        else:
+            return super().options(section, **kwargs)
+
+
+def get_greenflow_config_modules():
+    if 'GREENFLOW_CONFIG' not in os.environ:
+        os.environ['GREENFLOW_CONFIG'] = os.getcwd() + '/greenflowrc'
+        print('\nGREENFLOW_CONFIG NOT SET. SETTING TO: {}'
+              .format(os.environ['GREENFLOW_CONFIG']))
+
+    config = ConfigParser(defaults=os.environ)
+    greenflow_cfg = os.getenv('GREENFLOW_CONFIG', None)
+    if Path(greenflow_cfg).is_file():
+        config.read(greenflow_cfg)
+
+    if 'ModuleFiles' not in config:
+        return {}
+
+    modules_names = config.options('ModuleFiles')
+    modules_list = {imod: config['ModuleFiles'][imod]
+                    for imod in modules_names}
+
+    return modules_list
+
+
+# create a task to add path path
+def append_path(path):
+    if path not in sys.path:
+        sys.path.append(path)
+
+
+def import_submodules(package, recursive=True, _main_package=None):
+    """Import all submodules of a module, recursively, including subpackages.
+    Finds members of those packages. If a member is a greenflow Node subclass
+     then sets the top level package attribute with the class. This is done so
+      that the class can be accessed via:
+        NodeClass = getattr(mod, node_type)
+    Where mod is the package.
+
+    :param package: package (name or actual module)
+    :type package: module, str
+    """
+    if isinstance(package, str):
+        package = importlib.import_module(package)
+
+    _main_package = package if _main_package is None else _main_package
+    # for loader, name, is_pkg
+    for _, name, is_pkg in pkgutil.walk_packages(package.__path__):
+        full_name = package.__name__ + '.' + name
+        mod = importlib.import_module(full_name)
+        if recursive and is_pkg:
+            # import_submodules(full_name, _main_package=_main_package)
+            import_submodules(mod, _main_package=_main_package)
+
+        for node in inspect.getmembers(mod):
+            nodecls = node[1]
+            if not inspect.isclass(nodecls):
+                continue
+            if not issubclass(nodecls, _Node):
+                continue
+
+            setattr(_main_package, nodecls.__name__, nodecls)
+
+
+def load_modules(pathfile, name=None):
+    """
+    Given a py filename with path information,
+    It will load the file as a python
+    module, put it into the sys.modules and add the path into the pythonpath.
+    @param modulefile
+        string, file name
+    @returns
+        namedtuple, absolute path and loaded module
+    """
+    key = (pathfile, name)
+    if key in MODULE_CACHE:
+        return MODULE_CACHE[key]
+    filename = Path(pathfile)
+    if not filename.exists():
+        filename = get_file_path(str(filename))
+        filename = Path(filename)
+
+    if name is None:
+        modulename = filename.stem
+    else:
+        modulename = name
+
+    module_dir = str(filename.parent.absolute())
+
+    if filename.is_dir():
+        modulepath = filename.joinpath('__init__.py')
+        modulename = filename.stem
+    else:
+        modulepath = filename
+
+    spec = importlib.util.spec_from_file_location(modulename, str(modulepath))
+    mod = importlib.util.module_from_spec(spec)
+    sys.modules[spec.name] = mod
+    if filename.is_dir():
+        import_submodules(mod)
+
+    spec.loader.exec_module(mod)
+    Load = namedtuple("Load", "path mod")
+    loaded = Load(module_dir, mod)
+    MODULE_CACHE[key] = loaded
+    return loaded
+
+
+def get_node_tgraphmixin_instance(NodeClass, task):
+    '''Instantiate a node using task and NodeClass with NodeTaskGraphMixin
+    mixin. The returned node instance is a task graph aware node meaning it
+    can query task graph state such as its connections. Refer to the API of
+    NodeTaskGraphMixin.
+
+    :type NodeClass: a class type of a Node implementation
+    :type task: an instance of class::Task
+
+    '''
+
+    class NodeInTaskGraph(NodeTaskGraphMixin, NodeClass):
+
+        def __init__(self, task):
+            NodeClass.__init__(self, task)
+            NodeTaskGraphMixin.__init__(self)
+
+        def __repr__(self):
+            '''Override repr to show the name and path of the plugin
+            node class.'''
+            return '<{} {}.{} object at {}>'.format(
+                self.__class__.__name__,
+                NodeClass.__module__,
+                NodeClass.__name__,
+                hex(id(self)))
+
+    return NodeInTaskGraph(task)
+
+
+def get_node_obj(task, replace=None, profile=False, tgraph_mixin=False,
+                 dask_ray_setup=True):
+    """
+    Instantiate a node instance for a task given the replacement setup.
+
+    Arguments
+    -------
+    replace: dict
+        conf parameters replacement
+    profile: Boolean
+        profile the node computation
+
+    Returns
+    -----
+    object
+        Node instance
+    """
+    replace = dict() if replace is None else replace
+
+    task_spec = copy.copy(task._task_spec)
+    task_spec.update(replace)
+
+    # node_id = task_spec[TaskSpecSchema.task_id]
+    modulepath = task_spec.get(TaskSpecSchema.filepath)
+    module_name = task_spec.get(TaskSpecSchema.module)
+
+    node_type = task_spec[TaskSpecSchema.node_type]
+    task = Task(task_spec)
+
+    NodeClass = None
+    module_dir = None
+    if isinstance(node_type, str):
+        modules = get_greenflow_config_modules()
+        if modulepath is not None:
+            loaded = load_modules(modulepath)
+            module_dir = loaded.path
+            mod = loaded.mod
+            NodeClass = getattr(mod, node_type)
+        elif (module_name is not None):
+            if module_name in sys.modules:
+                mod = sys.modules[module_name]
+            else:
+                loaded = load_modules(
+                    modules[module_name], name=module_name)
+                module_dir = loaded.path
+                mod = loaded.mod
+            try:
+                NodeClass = getattr(mod, node_type)
+            except AttributeError:
+                pass
+        else:
+            try:
+                global DEFAULT_MODULE
+                plugmod = os.getenv('GREENFLOW_PLUGIN_MODULE',
+                                    DEFAULT_MODULE)
+                MODLIB = importlib.import_module(plugmod)
+                NodeClass = getattr(MODLIB, node_type)
+            except AttributeError:
+                for key in modules:
+                    loaded = load_modules(modules[key], name=key)
+                    module_dir = loaded.path
+                    mod = loaded.mod
+                    try:
+                        NodeClass = getattr(mod, node_type)
+                        break
+                    except AttributeError:
+                        continue
+
+        if NodeClass is None:
+            raise Exception("Cannot find the Node Class:" +
+                            node_type)
+
+        if module_dir:
+            append_path(module_dir)
+
+        if module_dir and dask_ray_setup:
+            try:
+                # Add python path to all the client workers
+                # assume all the workers share the same directory
+                # structure
+                import dask.distributed
+                client = dask.distributed.client.default_client()
+                client.run(append_path, module_dir)
+            except (ValueError, ImportError):
+                pass
+
+            try:
+                import ray
+
+                def ray_append_path(worker):
+                    import sys  # @Reimport
+                    if module_dir not in sys.path:
+                        sys.path.append(module_dir)
+
+                # TODO: This could be a Ray Driver functionality. Add
+                #     module path to all workers.
+                ray.worker.global_worker.run_function_on_all_workers(
+                    ray_append_path)
+            except (ValueError, ImportError):
+                pass
+
+    elif issubclass(node_type, _Node):
+        NodeClass = node_type
+    else:
+        raise Exception("Node type not supported: {}".format(node_type))
+
+    assert issubclass(NodeClass, _Node), \
+        'Node-type is not a subclass of "Node" class.'
+
+    if tgraph_mixin:
+        node = get_node_tgraphmixin_instance(NodeClass, task)
+    else:
+        node = NodeClass(task)
+
+    node.profile = profile
+
+    return node
diff --git a/greenflow/greenflow/dataframe_flow/metaSpec.py b/greenflow/greenflow/dataframe_flow/metaSpec.py
new file mode 100644
index 00000000..7fe82d4f
--- /dev/null
+++ b/greenflow/greenflow/dataframe_flow/metaSpec.py
@@ -0,0 +1,71 @@
+from greenflow._common import _namedtuple_with_defaults
+
+
+__all__ = ['MetaData', 'MetaDataSchema']
+
+_MetaData = _namedtuple_with_defaults(
+    '_MetaData',
+    ['inports', 'outports'],
+    {'inports': dict(), 'outports': dict()}
+)
+
+
+class MetaData(_MetaData):
+    '''Node metadata must be setup for inputs and outputs. The validation
+    logic will check whether the required inputs met the passed in output
+    metadata. and the produced calculation results matches the output
+    metadata.
+
+    :ivar inports: Dictionary defining input metadata, which specified the
+                   input requirement
+    :ivar outports: Dictionary defining output metadata
+
+    Empty dicts default:
+        metadata = MetaData()
+        metadata.inports and metadata.outports are empty dicts
+
+    Example with port specs:
+        inports = {
+            'iport0_name': {
+                "column0": "float64",
+                "column1": "float64",
+            },
+            'iport1_name': {
+                "column0": "float64",
+                "column1": "float64",
+                "column2": "float64",
+            }
+        }
+
+        outports = {
+            'oport0_name': {
+                "column0": "float64",
+                "column1": "float64",
+                "column2": "float64",
+            },
+            'oport1_name': {
+                "column0": "float64",
+                "column1": "float64",
+                "column2": "float64",
+                "column3": "float64",
+            }
+        }
+
+        metadata = MetaData(inports=inports, outports=outports)
+
+    The inports/outports are nested dictionaries. The outer dictionary is keyed
+    by port name with metadata obj being the value of the outer dictionary. The
+    metadata obj is a dictionary with keys/fields which can be serialized into
+    JSON.
+    '''
+
+
+class MetaDataSchema:
+    '''Explanation of the fields, etc.'''
+    META_OP_DELETION = 'deletion'
+    META_OP_ADDITION = 'addition'
+    META_OP_RETENTION = 'retention'
+    META_REF_INPUT = 'input'
+    META_OP = 'meta_op'
+    META_DATA = 'data'
+    META_ORDER = 'order'
diff --git a/greenflow/greenflow/dataframe_flow/node.py b/greenflow/greenflow/dataframe_flow/node.py
index 3b4dae45..296b91e5 100644
--- a/greenflow/greenflow/dataframe_flow/node.py
+++ b/greenflow/greenflow/dataframe_flow/node.py
@@ -1,10 +1,12 @@
+
 import abc
 from .task import Task
 from .taskSpecSchema import TaskSpecSchema
-from .portsSpecSchema import PortsSpecSchema, ConfSchema, MetaData, NodePorts
+from .portsSpecSchema import (ConfSchema, NodePorts)
+from .metaSpec import MetaData
 
 from ._node import _Node
-
+from ._node_extension_mixin import NodeExtensionMixin
 
 __all__ = ['Node']
 
@@ -36,11 +38,14 @@ def _get_output_ports(self, full_port_spec=False):
         return self.__get_io_port(io='out', full_port_spec=full_port_spec)
 
 
-class Node(_PortsMixin, _Node):
-    '''Base class for implementing greenflow plugins i.e. nodes. A node processes
-    tasks within a greenflow task graph.
+class Node(NodeExtensionMixin, _PortsMixin, _Node):
+    '''Base class for implementing greenflow plugins i.e. nodes. A node
+    processes tasks within a greenflow task graph.
+
+    Within the context of a task graph the Node class is mixed in with classes
+    NodeTaskGraphMixin and NodeTaskGraphExtensionMixin.
 
-    must implement the following method:
+    Must implement the following method:
 
         :meth: ports_setup
             Defines ports for the node. Refer to ports_setup docstring for
@@ -93,16 +98,22 @@ def __init__(self, task):
 
         self.delayed_process = False
         # eargerly infer the metadata, costly
-        self.infer_meta = True
+        self.infer_meta = False
         # customized the column setup
         self.init()
         self.profile = False  # by default, do not profile
 
-        PortsSpecSchema.validate_ports(self.ports_setup())
+        # PortsSpecSchema.validate_ports(self.ports_setup())
 
+    @abc.abstractmethod
     def ports_setup(self) -> NodePorts:
         """Virtual method for specifying inputs/outputs ports.
 
+        Note: Within the context of a task graph the
+            NodeTaskGraphMixin.ports_setup is invoked and forwards the call
+            to the Node's implementation class ports_setup that is
+            implementation of this method.
+
         Must return an instance of NodePorts that adheres to PortsSpecSchema.
         Refer to PortsSpecSchema and NodePorts in module:
             greenflow.dataframe_flow.portsSpecSchema
@@ -157,8 +168,14 @@ def outport_connected(self, portname) -> bool:
 
     def update(self):
         """
-        a function is called after init, it used to update dynamic information
-        from parent nodes
+        Use the update method when relying on the dynamic information from
+        the parent nodes.
+
+        Call the self._resolve_ports and self._resolve_meta within update
+        if using ports and meta templates. Refer to class NodeExtensionMixin
+        and corresponding methods _resolve_ports and _resolve_meta.
+        Refer to usage examples of class TemplateNodeMixin.update.
+
         """
         pass
 
@@ -175,8 +192,8 @@ def get_connected_inports(self) -> dict:
 
     def init(self):
         """
-        Initialize the node. Usually it is used to self.delayed_process flag
-         and other special initialzation.
+        Initialize the node. Usually it is used to set self.delayed_process
+        flag and other special initialzation.
 
         The self.delayed_process flag is by default set to False. It can be
         overwritten here to True. For native dataframe API calls, dask cudf
@@ -189,12 +206,17 @@ def init(self):
         """
         pass
 
-    def get_input_meta(self) -> dict:
+    def get_input_meta(self, port_name=None):
         """
         Get the input meta information. It is usually used by individual
          node to compute the output meta information
+        if port_name is None
         returns
             dict, key is the node input port name, value is the metadata dict
+        if port_name is provided
+        returns
+            the meta data send to the input port with name `port_name`. If it
+            is not connected, return None
         """
         # this method will be implemented by NodeTaskGraphMixin
         return {}
@@ -224,6 +246,11 @@ def meta_setup(self) -> MetaData:
         All children class should implement this.
         It is used to compute the required input and output meta data.
 
+        Note: Within the context of a task graph the
+            NodeTaskGraphMixin.meta_setup is invoked and forwards the call
+            to the Node's implementation class meta_setup that is
+            implementation of this method.
+
         `inports` defines the required metadata for the node.
         metadata is python dictionaries, which can be serialized into JSON.
         The output metadata are calcuated based on the input meta data. It is
@@ -235,7 +262,7 @@ def meta_setup(self) -> MetaData:
         return MetaData(inports={}, outports={})
 
     @abc.abstractmethod
-    def process(self, inputs) -> dict:
+    def process(self, inputs, **kwargs) -> dict:
         """
         process the input dataframe. Children class is required to override
         this
diff --git a/greenflow/greenflow/dataframe_flow/portsSpecSchema.py b/greenflow/greenflow/dataframe_flow/portsSpecSchema.py
index 1d570a4d..58f0807b 100644
--- a/greenflow/greenflow/dataframe_flow/portsSpecSchema.py
+++ b/greenflow/greenflow/dataframe_flow/portsSpecSchema.py
@@ -4,7 +4,7 @@
 
 from greenflow._common import _namedtuple_with_defaults
 
-__all__ = ['PortsSpecSchema', 'NodePorts', 'ConfSchema', 'MetaData']
+__all__ = ['PortsSpecSchema', 'NodePorts', 'ConfSchema']
 
 
 _NodePorts = _namedtuple_with_defaults(
@@ -19,12 +19,6 @@
     {'json': dict(), 'ui': dict()}
 )
 
-_MetaData = _namedtuple_with_defaults(
-    '_MetaData',
-    ['inports', 'outports'],
-    {'inports': dict(), 'outports': dict()}
-)
-
 
 class NodePorts(_NodePorts):
     '''Node ports must be defined for inputs and outputs.
@@ -106,14 +100,17 @@ class PortsSpecSchema(object):
     port_type = 'type'
     optional = 'optional'
     dynamic = 'dynamic'
+    DYN_MATCH = 'matching_outputs'
 
     @classmethod
     def _typecheck(cls, schema_field, value):
         if (schema_field == cls.port_type):
             def check_ptype(val):
                 err_msg = 'Port type must be a pythonic '\
-                    'type i.e type(port_type) == type. Instead got: {}'
-                assert isinstance(val, type), err_msg.format(type(val))
+                    'type i.e type(port_type) == type. Instead got: {}, {}'
+                assert isinstance(val,
+                                  type), err_msg.format(type(val), val)
+
             if isinstance(value, Iterable):
                 for ptype in value:
                     check_ptype(ptype)
@@ -164,53 +161,3 @@ def validate_ports(cls, node_ports):
 
             for port_field, field_val in port_spec.items():
                 cls._typecheck(port_field, field_val)
-
-
-class MetaData(_MetaData):
-    '''Node metadata must be setup for inputs and outputs. The validation
-    logic will check whether the required inputs met the passed in output
-    metadata. and the produced calculation results matches the output
-    metadata.
-
-    :ivar inports: Dictionary defining input metadata, which specified the
-                   input requirement
-    :ivar outports: Dictionary defining output metadata
-
-    Empty dicts default:
-        metadata = MetaData()
-        metadata.inports and metadata.outports are empty dicts
-
-    Example with port specs:
-        inports = {
-            'iport0_name': {
-                "column0": "float64",
-                "column1": "float64",
-            },
-            'iport1_name': {
-                "column0": "float64",
-                "column1": "float64",
-                "column2": "float64",
-            }
-        }
-
-        outports = {
-            'oport0_name': {
-                "column0": "float64",
-                "column1": "float64",
-                "column2": "float64",
-            },
-            'oport1_name': {
-                "column0": "float64",
-                "column1": "float64",
-                "column2": "float64",
-                "column3": "float64",
-            }
-        }
-
-        metadata = MetaData(inports=inports, outports=outports)
-
-    The inports/outports are nested dictionaries. The outer dictionary is keyed
-    by port name with metadata obj being the value of the outer dictionary. The
-    metadata obj is a dictionary with keys/fields which can be serialized into
-    JSON.
-    '''
diff --git a/greenflow/greenflow/dataframe_flow/task.py b/greenflow/greenflow/dataframe_flow/task.py
index ca348808..7addcf39 100644
--- a/greenflow/greenflow/dataframe_flow/task.py
+++ b/greenflow/greenflow/dataframe_flow/task.py
@@ -1,129 +1,12 @@
-import os
-import importlib
-import pkgutil
-import inspect
 import copy
 from .taskSpecSchema import TaskSpecSchema
-from ._node import _Node
 from ._node_flow import OUTPUT_ID
-from pathlib import Path
-import sys
-from collections import namedtuple
-import configparser
-from .util import get_file_path
 
 
-__all__ = ['Task']
-
-DEFAULT_MODULE = os.getenv('GREENFLOW_PLUGIN_MODULE', "greenflow.plugin_nodes")
-
-
-class ConfigParser(configparser.ConfigParser):
-    """Can get options() without defaults
-    """
-
-    def options(self, section, no_defaults=True, **kwargs):
-        if no_defaults:
-            try:
-                return list(self._sections[section].keys())
-            except KeyError:
-                raise configparser.NoSectionError(section)
-        else:
-            return super().options(section, **kwargs)
-
-
-def get_greenflow_config_modules():
-    if 'GREENFLOW_CONFIG' not in os.environ:
-        os.environ['GREENFLOW_CONFIG'] = os.getcwd()+'/greenflowrc'
-        print(os.environ['GREENFLOW_CONFIG'])
-    config = ConfigParser(defaults=os.environ)
-    greenflow_cfg = os.getenv('GREENFLOW_CONFIG', None)
-    if Path(greenflow_cfg).is_file():
-        config.read(greenflow_cfg)
-    if 'ModuleFiles' not in config:
-        return {}
-    modules_names = config.options('ModuleFiles')
-    modules_list = {imod: config['ModuleFiles'][imod]
-                    for imod in modules_names}
-    return modules_list
-
-
-# create a task to add path path
-def append_path(path):
-    if path not in sys.path:
-        sys.path.append(path)
-
-
-def import_submodules(package, recursive=True, _main_package=None):
-    """Import all submodules of a module, recursively, including subpackages.
-    Finds members of those packages. If a member is a greenflow Node subclass
-     then sets the top level package attribute with the class. This is done so
-      that the class can be accessed via:
-        NodeClass = getattr(mod, node_type)
-    Where mod is the package.
-
-    :param package: package (name or actual module)
-    :type package: module, str
-    """
-    if isinstance(package, str):
-        package = importlib.import_module(package)
-
-    _main_package = package if _main_package is None else _main_package
-    # for loader, name, is_pkg
-    for _, name, is_pkg in pkgutil.walk_packages(package.__path__):
-        full_name = package.__name__ + '.' + name
-        mod = importlib.import_module(full_name)
-        if recursive and is_pkg:
-            # import_submodules(full_name, _main_package=_main_package)
-            import_submodules(mod, _main_package=_main_package)
-
-        for node in inspect.getmembers(mod):
-            nodecls = node[1]
-            if not inspect.isclass(nodecls):
-                continue
-            if not issubclass(nodecls, _Node):
-                continue
-
-            setattr(_main_package, nodecls.__name__, nodecls)
-
-
-def load_modules(pathfile, name=None):
-    """
-    Given a py filename with path information,
-    It will load the file as a python
-    module, put it into the sys.modules and add the path into the pythonpath.
-    @param modulefile
-        string, file name
-    @returns
-        namedtuple, absolute path and loaded module
-    """
-    filename = Path(pathfile)
-    if not filename.exists():
-        filename = get_file_path(str(filename))
-        filename = Path(filename)
-
-    if name is None:
-        modulename = filename.stem
-    else:
-        modulename = name
-
-    module_dir = str(filename.parent.absolute())
-
-    if filename.is_dir():
-        modulepath = filename.joinpath('__init__.py')
-        modulename = filename.stem
-    else:
-        modulepath = filename
+module_cache = {}
 
-    spec = importlib.util.spec_from_file_location(modulename, str(modulepath))
-    mod = importlib.util.module_from_spec(spec)
-    sys.modules[spec.name] = mod
-    if filename.is_dir():
-        import_submodules(mod)
 
-    spec.loader.exec_module(mod)
-    Load = namedtuple("Load", "path mod")
-    return Load(module_dir, mod)
+__all__ = ['Task']
 
 
 class Task(object):
@@ -155,133 +38,6 @@ def set_output(self):
     def get(self, key, default=None):
         return self._task_spec.get(key, default)
 
-    def get_node_obj(self, replace=None, profile=False, tgraph_mixin=False):
-        """
-        instantiate a node instance for this task given the replacement setup
-
-        Arguments
-        -------
-        replace: dict
-            conf parameters replacement
-        profile: Boolean
-            profile the node computation
-
-        Returns
-        -----
-        object
-            Node instance
-        """
-        replace = dict() if replace is None else replace
-
-        task_spec = copy.copy(self._task_spec)
-        task_spec.update(replace)
-
-        # node_id = task_spec[TaskSpecSchema.task_id]
-        modulepath = task_spec.get(TaskSpecSchema.filepath)
-        module_name = task_spec.get(TaskSpecSchema.module)
-
-        node_type = task_spec[TaskSpecSchema.node_type]
-        task = Task(task_spec)
-
-        NodeClass = None
-        module_dir = None
-        if isinstance(node_type, str):
-            modules = get_greenflow_config_modules()
-            if modulepath is not None:
-                loaded = load_modules(modulepath)
-                module_dir = loaded.path
-                mod = loaded.mod
-                NodeClass = getattr(mod, node_type)
-            elif (module_name is not None):
-                if module_name in sys.modules:
-                    mod = sys.modules[module_name]
-                else:
-                    loaded = load_modules(
-                        modules[module_name], name=module_name)
-                    module_dir = loaded.path
-                    mod = loaded.mod
-                try:
-                    NodeClass = getattr(mod, node_type)
-                except AttributeError:
-                    pass
-            else:
-                try:
-                    global DEFAULT_MODULE
-                    plugmod = os.getenv('GREENFLOW_PLUGIN_MODULE',
-                                        DEFAULT_MODULE)
-                    MODLIB = importlib.import_module(plugmod)
-                    NodeClass = getattr(MODLIB, node_type)
-                except AttributeError:
-                    for key in modules:
-                        loaded = load_modules(modules[key], name=key)
-                        module_dir = loaded.path
-                        mod = loaded.mod
-                        try:
-                            NodeClass = getattr(mod, node_type)
-                            break
-                        except AttributeError:
-                            continue
-            if NodeClass is None:
-                raise Exception("Cannot find the Node Class:" +
-                                node_type)
-            if module_dir:
-                append_path(module_dir)
-                try:
-                    # Add python path to all the client workers
-                    # assume all the workers share the same directory
-                    # structure
-                    import dask.distributed
-                    client = dask.distributed.client.default_client()
-                    client.run(append_path, module_dir)
-                except (ValueError, ImportError):
-                    pass
-
-                try:
-                    import ray
-
-                    def ray_append_path(worker):
-                        import sys  # @Reimport
-                        if module_dir not in sys.path:
-                            sys.path.append(module_dir)
-
-                    # TODO: This could be a Ray Driver functionality. Add
-                    #     module path to all workers.
-                    ray.worker.global_worker.run_function_on_all_workers(
-                        ray_append_path)
-                except (ValueError, ImportError):
-                    pass
-
-        elif issubclass(node_type, _Node):
-            NodeClass = node_type
-        else:
-            raise Exception("Node type not supported: {}".format(node_type))
-
-        assert issubclass(NodeClass, _Node), \
-            'Node-type is not a subclass of "Node" class.'
-
-        if tgraph_mixin:
-            from ._node_flow import NodeTaskGraphMixin
-
-            class NodeInTaskGraph(NodeTaskGraphMixin, NodeClass):
-                def __init__(self, task):
-                    NodeClass.__init__(self, task)
-                    NodeTaskGraphMixin.__init__(self)
-
-                def __repr__(self):
-                    '''Override repr to show the name and path of the plugin
-                    node class.'''
-                    return '<{} {}.{} object at {}>'.format(
-                        self.__class__.__name__,
-                        NodeClass.__module__,
-                        NodeClass.__name__,
-                        hex(id(self)))
-
-            node = NodeInTaskGraph(task)
-        else:
-            node = NodeClass(task)
-        node.profile = profile
-        return node
-
 
 if __name__ == "__main__":
     t = {'id': 'test',
diff --git a/greenflow/greenflow/dataframe_flow/taskGraph.py b/greenflow/greenflow/dataframe_flow/taskGraph.py
index 5c141dc0..70149b18 100644
--- a/greenflow/greenflow/dataframe_flow/taskGraph.py
+++ b/greenflow/greenflow/dataframe_flow/taskGraph.py
@@ -1,17 +1,19 @@
-from collections import OrderedDict
-import ruamel.yaml
-from .node import Node
-from ._node_flow import OUTPUT_ID, OUTPUT_TYPE, _CLEANUP
-from .task import Task
-from .taskSpecSchema import TaskSpecSchema
-from .portsSpecSchema import NodePorts, ConfSchema
 import warnings
 import copy
 import traceback
 import cloudpickle
 import base64
 from types import ModuleType
+from collections import OrderedDict
+import ruamel.yaml
+
+from .node import Node
+from ._node_flow import OUTPUT_ID, OUTPUT_TYPE, _CLEANUP
+from .task import Task
+from .taskSpecSchema import TaskSpecSchema
+from .portsSpecSchema import NodePorts, ConfSchema, PortsSpecSchema
 from .util import get_encoded_class
+from .config_nodes_modules import get_node_obj
 
 __all__ = ['TaskGraph', 'OutputCollector']
 
@@ -236,8 +238,8 @@ class into the Jupyterlab kernel space, communicate with the
               will automatically construct the hierachical menus based on '.'
 
         class_obj: Node
-            The node class that is the subclass of greenflow 'Node'. It is usually
-            defined dynamically so it can be registered.
+            The node class that is the subclass of greenflow 'Node'. It is
+             usually defined dynamically so it can be registered.
 
         Returns
         -----
@@ -351,7 +353,7 @@ def viz_graph(self, show_ports=False):
 
                 if (to_type == OUTPUT_TYPE):
                     continue
-                task_node = itask.get_node_obj()
+                task_node = get_node_obj(itask)
                 # task_outputs = itask.get(TaskSpecSchema.outputs, [])
                 for pout in task_node._get_output_ports():
                     out_tip = '{}.{}'.format(
@@ -363,7 +365,7 @@ def viz_graph(self, show_ports=False):
                         'shape': 'point'})
         return G
 
-    def build(self, replace=None, profile=False):
+    def _build(self, replace=None, profile=False):
         """
         compute the graph structure of the nodes. It will set the input and
         output nodes for each of the node
@@ -395,10 +397,10 @@ def build(self, replace=None, profile=False):
                     TaskSpecSchema.node_type: OutputCollector,
                     TaskSpecSchema.inputs: task[TaskSpecSchema.inputs]
                 })
-                node = output_task.get_node_obj(tgraph_mixin=True)
+                node = get_node_obj(output_task, tgraph_mixin=True)
             else:
-                node = task.get_node_obj(replace.get(task_id), profile,
-                                         tgraph_mixin=True)
+                node = get_node_obj(task, replace.get(task_id), profile,
+                                    tgraph_mixin=True)
             self.__node_dict[task_id] = node
 
         # build the graph
@@ -434,13 +436,56 @@ def build(self, replace=None, profile=False):
                     'from_port': src_port
                 })
 
+    def build(self, replace=None, profile=False):
+        """
+        compute the graph structure of the nodes. It will set the input and
+        output nodes for each of the node
+
+        Arguments
+        -------
+        replace: dict
+            conf parameters replacement
+        """
+        # make connection only
+        self._build(replace=replace, profile=profile)
+
         # Columns type checking is done in the :meth:`TaskGraph._run` after the
         # outputs are specified and participating tasks are determined.
 
         # this part is to update each of the node so dynamic inputs can be
         # processed
+        self.breadth_first_update()
+
+    def breadth_first_update(self, extra_roots=[], extra_updated=set()):
+        """
+        Do a breadth first graph traversal and update nodes.
+
+        Update each note following the causal order. The children notes are
+        only added to the queue if all the parents are updated.
+
+        Each node is only updated once.
+
+        extra_roots and extra_updated should be empty for normal graph. It
+        is used for composite node when the graph is connected to other
+        graph.
+        """
+        queue = []
+        updated = extra_updated
         for k in self.__node_dict.keys():
-            self.__node_dict[k].update()
+            if len(self.__node_dict[k].inputs) == 0:
+                queue.append(self.__node_dict[k])
+        queue.extend(extra_roots)
+        while (len(queue) != 0):
+            node_to_update = queue.pop(0)
+            # print('update {}'.format(node_to_update.uid))
+            if node_to_update not in updated:
+                node_to_update.update()
+            updated.add(node_to_update)
+            for element in node_to_update.outputs:
+                child = element['to_node']
+                if all([i['from_node'] in updated for i in child.inputs]):
+                    queue.append(child)
+        # print('----done----')
 
     def __getitem__(self, key):
         return self.__node_dict[key]
@@ -500,8 +545,8 @@ def _run(self, outputs=None, replace=None, profile=False, formated=False):
                 TaskSpecSchema.inputs: []
             })
 
-            outputs_collector_node = output_task.get_node_obj(
-                tgraph_mixin=True)
+            outputs_collector_node = get_node_obj(output_task,
+                                                  tgraph_mixin=True)
 
         outputs_collector_node.clear_input = False
         if not found_output_node or outputs is not None:
@@ -546,6 +591,7 @@ def _run(self, outputs=None, replace=None, profile=False, formated=False):
                 continue
 
             # Run ports validation.
+            PortsSpecSchema.validate_ports(node.ports_setup())
             node.validate_connected_ports()
 
             # Run meta setup in case the required meta are calculated
@@ -596,7 +642,7 @@ def cleanup():
         ####
         # this is for nemo work around, to clean up the nemo graph
         self.run_cleanup()
-        ####
+
         if formated:
             return formated_result(result)
         else:
diff --git a/greenflow/greenflow/dataframe_flow/template_node_mixin.py b/greenflow/greenflow/dataframe_flow/template_node_mixin.py
new file mode 100644
index 00000000..90f38456
--- /dev/null
+++ b/greenflow/greenflow/dataframe_flow/template_node_mixin.py
@@ -0,0 +1,169 @@
+from greenflow.dataframe_flow.portsSpecSchema import NodePorts
+from greenflow.dataframe_flow.metaSpec import MetaData
+
+
+__all__ = ['TemplateNodeMixin']
+
+
+class TemplateNodeMixin:
+    '''This mixin is used with Nodes that use attributes for managing ports
+    and meta.
+
+    :ivar port_inports: Ports dictionary for input ports.
+    :ivar port_outports: Ports dictionary for output ports.
+    :ivar meta_inports: Metadata dictionary for input ports.
+    :ivar meta_outports: Metadata dictionary for output ports.
+    '''
+
+    def init(self):
+        """
+        Used to initilze the Node. called from the node constructor
+        all children should run parent init first in the constructor e.g.
+        def init(self):
+            TemplateNodeMixin.init(self)
+            ....
+
+        In this function. Define the static ports and meta setup. Note,
+        only static information can be used includig the self.conf
+        information. If need information from
+        self.get_connected_inports() and self.get_input_meta(),
+        please define it in update() function.
+
+        Define the template ports setup by self.template_ports_setup
+        E.g.
+
+        port_inports = {
+            "port0_name": {
+                PortsSpecSchema.port_type: ["type0", "type1"]
+            },
+            "port1_name": {
+                PortsSpecSchema.port_type: "${conf:some_type}",
+                PortsSpecSchema.dynamic: {
+                    # choie can be True/False, list of types or string
+                    # True, generate outports matching the
+                    # connected dynamic input ports, use the
+                    # the same type as the dynamic port
+                    # False, not generate matching outports
+                    # list of types or string, same as True condition,
+                    # but use the specified types
+                    PortsSpecSchema.DYN_MATCH: ['type0', 'type1']
+                }
+            },
+            ...
+        }
+        port_outports = {
+            "port0_name": {
+                PortsSpecSchema.port_type: ["type0", "type1"]
+            },
+            "port1_name": {
+                PortsSpecSchema.port_type: "${port:port0_name}"
+            },
+            ...
+        }
+        self.template_ports_setup(in_ports=port_inports, 
+                                  out_ports=port_outports)
+
+        Define the template meta data setup by self.template_meta_setup.
+        E.g.
+
+        meta_inports = {
+            "port0_name": {
+                "name0": "type0",
+                "name1": "type1",
+                "name2": "type2",
+            },
+            "port1_name": {
+                "${conf:abc}": "type0",
+                "name1": "type1",
+                "name2": "${conf:type1}",
+            },
+            ...
+        }
+        meta_outports = {
+            "port0_name": {
+                MetaDataSchema.META_REF_INPUT: "port0_name",
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_DATA: {
+                    "${conf:abc}": "type0",
+                    "name1": "type1",
+                    "name2": "${conf:type1}",
+                }
+                # order is optional
+                MetaDataSchema.META_ORDER: {
+                    "${conf:abc}": 0,
+                    "name2": -1
+                }
+            },
+            "port1_name": {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_RETENTION,
+                MetaDataSchema.META_DATA: {
+                    "${conf:abc}": "type0",
+                    "name1": "type1",
+                    "name2": "${conf:type1}",
+                },
+                # order is optional
+                MetaDataSchema.META_ORDER: {
+                    "${conf:abc}": -1,
+                }
+            },
+            ...
+        }
+        self.template_meta_setup(in_ports=meta_inports,
+                                 out_ports=meta_outports)
+        """
+        if not hasattr(self, '__port_inports'):
+            self.__port_inports = {}
+
+        if not hasattr(self, '__port_outports'):
+            self.__port_outports = {}
+
+        if not hasattr(self, '__meta_inports'):
+            self.__meta_inports = {}
+
+        if not hasattr(self, '__meta_outports'):
+            self.__meta_outports = {}
+
+    def template_ports_setup(self, in_ports=None, out_ports=None):
+        if in_ports is not None:
+            self.__port_inports = in_ports
+        if out_ports is not None:
+            self.__port_outports = out_ports
+        return NodePorts(inports=self.__port_inports,
+                         outports=self.__port_outports)
+
+    def template_meta_setup(self, in_ports=None, out_ports=None):
+        if in_ports is not None:
+            self.__meta_inports = in_ports
+        if out_ports is not None:
+            self.__meta_outports = out_ports
+        return MetaData(inports=self.__meta_inports,
+                        outports=self.__meta_outports)
+
+    def update(self):
+        '''Updates state of a Node with resolved ports and meta.
+        '''
+        ports_template = \
+            NodePorts(inports=self.__port_inports,
+                      outports=self.__port_outports)
+        ports = self._resolve_ports(ports_template)
+        port_inports = ports.inports
+        meta_template = \
+            MetaData(inports=self.__meta_inports,
+                     outports=self.__meta_outports)
+        meta = self._resolve_meta(meta_template, port_inports)
+
+        self.__port_inports = ports.inports
+        self.__port_outports = ports.outports
+
+        self.__meta_inports = meta.inports
+        self.__meta_outports = meta.outports
+
+    def ports_setup(self):
+        ports = NodePorts(inports=self.__port_inports,
+                          outports=self.__port_outports)
+        return self.ports_setup_ext(ports)
+
+    def meta_setup(self):
+        meta = MetaData(inports=self.__meta_inports,
+                        outports=self.__meta_outports)
+        return self.meta_setup_ext(meta)
diff --git a/greenflow/greenflow/flow.py b/greenflow/greenflow/flow.py
index ca69100b..9e3195c9 100644
--- a/greenflow/greenflow/flow.py
+++ b/greenflow/greenflow/flow.py
@@ -11,7 +11,7 @@ def main():
     import pudb
     pudb.set_trace()
 
-    task_graph = TaskGraph.load_workflow(args.task)
+    task_graph = TaskGraph.load_taskgraph(args.task)
     print('output nodes:', args.output)
     task_graph.run(args.output)
 
diff --git a/greenflow/greenflow/plugin_nodes/util/compositeNode.py b/greenflow/greenflow/plugin_nodes/util/compositeNode.py
index 3b677855..f863c5d6 100644
--- a/greenflow/greenflow/plugin_nodes/util/compositeNode.py
+++ b/greenflow/greenflow/plugin_nodes/util/compositeNode.py
@@ -1,13 +1,11 @@
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
 from greenflow.dataframe_flow import Node
 from greenflow.dataframe_flow import TaskGraph
 from greenflow.dataframe_flow.taskSpecSchema import TaskSpecSchema
 from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
-from greenflow.dataframe_flow.portsSpecSchema import NodePorts, MetaData
-from greenflow.dataframe_flow.cache import (CACHE_META,
-                                         CACHE_PORTS, CACHE_SCHEMA)
-import json
+from greenflow.dataframe_flow.portsSpecSchema import NodePorts
+from greenflow.dataframe_flow.metaSpec import MetaData
 import os
-import hashlib
 from greenflow.dataframe_flow.util import get_file_path
 import uuid
 
@@ -49,32 +47,17 @@ def group_ports(input_list):
     return nodes_group
 
 
-class CompositeNode(Node):
+class CompositeNode(TemplateNodeMixin, Node):
+
+    def init(self):
+        TemplateNodeMixin.init(self)
+        self.task_graph = None
 
     def update(self):
+        TemplateNodeMixin.update(self)
         self.conf_update()  # update the conf
-
-    def conf_update(self):
-        """
-        This method is used to overwrite the conf from
-        external sources
-        """
-        pass
-
-    def _compute_hash_key(self):
-        """
-        if hash changed, the port_setup, meta_setup
-        and conf_json should be different
-        In very rara case, might have the problem of hash collision,
-        It affects the column, port and conf calculation. It won't
-        change the computation result though.
-        It returns the hash code, the loaded task_graph,
-        the replacement conf obj
-        """
         task_graph = ""
-        inputs = ()
         replacementObj = {}
-        input_node = ""
         task_graph_obj = None
         if 'taskgraph' in self.conf:
             try:
@@ -82,21 +65,88 @@ def _compute_hash_key(self):
             except FileNotFoundError:
                 task_graph = None
             if task_graph is not None and os.path.exists(task_graph):
-                with open(task_graph) as f:
-                    task_graph = hashlib.md5(f.read().encode()).hexdigest()
+                # with open(task_graph) as f:
+                #     task_graph = hashlib.md5(f.read().encode()).hexdigest()
                 task_graph_obj = TaskGraph.load_taskgraph(
                     get_file_path(self.conf['taskgraph']))
+        self.all_inputs = []
+        self.all_outputs = []
+        self.task_graph = task_graph_obj
         self.update_replace(replacementObj, task_graph_obj)
-        if 'input' in self.conf:
-            for inp in self.conf['input']:
-                input_node += inp+","
-                if hasattr(self, 'inputs'):
-                    for i in self.inputs:
-                        inputs += (hash(i['from_node']),
-                                   i['to_port'], i['from_port'])
-        return (hash((self.uid, task_graph, inputs, json.dumps(self.conf),
-                      input_node, json.dumps(replacementObj))), task_graph_obj,
-                replacementObj)
+        self.replacementObj = replacementObj
+        extra_updated = set()
+        extra_roots = []
+        if self.task_graph is not None:
+            self.task_graph._build(replace=self.replacementObj)
+            if 'input' in self.conf:
+                # group input ports by node id
+                self.inp_groups = group_ports(self.conf['input'])
+                for inp in self.inp_groups.keys():
+                    if inp in self.task_graph:
+                        inputNode = self.task_graph[inp]
+                        update_inputs = []
+                        replaced_ports = set(self.inp_groups[inp])
+                        for oldInput in inputNode.inputs:
+                            if oldInput['to_port'] in replaced_ports:
+                                # we want to disconnect this old one and
+                                # connect to external node
+                                if hasattr(self, 'inputs'):
+                                    for externalInput in self.inputs:
+                                        if (_get_node(externalInput['to_port'])
+                                                == inputNode.uid and _get_port(
+                                                    externalInput['to_port'])
+                                                == oldInput['to_port']):
+                                            newInput = {}
+                                            newInput['to_port'] = _get_port(
+                                                externalInput['to_port'])
+                                            newInput[
+                                                'from_port'] = externalInput[
+                                                    'from_port']
+                                            newInput[
+                                                'from_node'] = externalInput[
+                                                    'from_node']
+                                            update_inputs.append(newInput)
+                            else:
+                                update_inputs.append(oldInput)
+                        inputNode.inputs = update_inputs
+
+                        # add all the `updated` parents to the set
+                        for i in inputNode.inputs:
+                            if hasattr(i['from_node'], 'ports_setup_cache'):
+                                extra_updated.add(i['from_node'])
+                        # if all the parents are updated, this is
+                        # a new root node
+                        if all([
+                                i['from_node'] in extra_updated
+                                for i in inputNode.inputs
+                        ]):
+                            extra_roots.append(inputNode)
+
+                        self.all_inputs.append((inputNode, inp))
+
+            if 'output' in self.conf:
+                self.oup_groups = group_ports(self.conf['output'])
+                for oup in self.oup_groups.keys():
+                    if oup in self.task_graph:
+                        outNode = self.task_graph[oup]
+                        # we do not disconnect anything here, as we take extra
+                        # outputs for composite node.
+                        # Node, we rely on the fact that taskgraph.run method
+                        # will remove the output collector from taskgraph if
+                        # the outputlist is set
+                        self.all_outputs.append((outNode, oup))
+                        # outNode_fun(outNode, oup_groups[oup])
+
+            # update all the nodes and cache it
+            self.task_graph.breadth_first_update(extra_roots=extra_roots,
+                                                 extra_updated=extra_updated)
+
+    def conf_update(self):
+        """
+        This method is used to overwrite the conf from
+        external sources
+        """
+        pass
 
     def _make_sub_graph_connection(self, task_graph,
                                    inputNode_fun,
@@ -109,62 +159,20 @@ def _make_sub_graph_connection(self, task_graph,
         outputNode_fun has subgraph outputNode and all the outpout ports
         as argument, it processes the outNode logics
         """
-        if 'input' in self.conf:
-            # group input ports by node id
-            inp_groups = group_ports(self.conf['input'])
-            for inp in inp_groups.keys():
-                if inp in task_graph:
-                    inputNode = task_graph[inp]
-                    update_inputs = []
-                    replaced_ports = set(inp_groups[inp])
-                    for oldInput in inputNode.inputs:
-                        if oldInput['to_port'] in replaced_ports:
-                            # we want to disconnect this old one and
-                            # connect to external node
-                            if hasattr(self, 'inputs'):
-                                for externalInput in self.inputs:
-                                    if (_get_node(
-                                        externalInput[
-                                            'to_port']) == inputNode.uid
-                                            and _get_port(
-                                                externalInput[
-                                                    'to_port']) == oldInput[
-                                                        'to_port']):
-                                        newInput = {}
-                                        newInput['to_port'] = _get_port(
-                                            externalInput['to_port'])
-                                        newInput['from_port'] = externalInput[
-                                            'from_port']
-                                        newInput['from_node'] = externalInput[
-                                            'from_node']
-                                        update_inputs.append(newInput)
-                        else:
-                            update_inputs.append(oldInput)
-                    inputNode.inputs = update_inputs
-                    inputNode_fun(inputNode, inp_groups[inp])
-
-        if 'output' in self.conf:
-            oup_groups = group_ports(self.conf['output'])
-            for oup in oup_groups.keys():
-                if oup in task_graph:
-                    outNode = task_graph[oup]
-                    # we do not disconnect anything here, as we take extra
-                    # outputs for composite node.
-                    # Node, we rely on the fact that taskgraph.run method
-                    # will remove the output collector from taskgraph if
-                    # the outputlist is set
-                    outNode_fun(outNode, oup_groups[oup])
+        for innode in self.all_inputs:
+            inputNode_fun(innode[0], self.inp_groups[innode[1]])
+        for outnode in self.all_outputs:
+            # inputNode_fun(innode[0], inp_groups[innode[1]])
+            outNode_fun(outnode[0], self.oup_groups[outnode[1]])
+        # this part is to update each of the node so dynamic inputs can be
+        # processed
+        # task_graph.cache_update_result()
 
     def ports_setup(self):
-        cache_key, task_graph, replacementObj = self._compute_hash_key()
-        if cache_key in CACHE_PORTS:
-            # print('cache hit')
-            return CACHE_PORTS[cache_key]
+        task_graph = self.task_graph
         inports = {}
         outports = {}
         if task_graph:
-            task_graph.build(replace=replacementObj)
-
             def inputNode_fun(inputNode, in_ports):
                 inport = {}
                 before_fix = inputNode.ports_setup().inports
@@ -184,19 +192,13 @@ def outNode_fun(outNode, out_ports):
             self._make_sub_graph_connection(task_graph,
                                             inputNode_fun, outNode_fun)
         output_port = NodePorts(inports=inports, outports=outports)
-        CACHE_PORTS[cache_key] = output_port
         return output_port
 
     def meta_setup(self):
-        cache_key, task_graph, replacementObj = self._compute_hash_key()
-        if cache_key in CACHE_META:
-            # print('cache hit')
-            return CACHE_META[cache_key]
+        task_graph = self.task_graph
         required = {}
         out_meta = {}
         if task_graph:
-            task_graph.build(replace=replacementObj)
-
             def inputNode_fun(inputNode, in_ports):
                 req = {}
                 # do meta_setup so required columns are ready
@@ -217,15 +219,12 @@ def outNode_fun(outNode, out_ports):
 
             self._make_sub_graph_connection(task_graph,
                                             inputNode_fun, outNode_fun)
+
         metadata = MetaData(inports=required, outports=out_meta)
-        CACHE_META[cache_key] = metadata
         return metadata
 
     def conf_schema(self):
-        cache_key, task_graph, replacementObj = self._compute_hash_key()
-        if cache_key in CACHE_SCHEMA:
-            # print('cache hit')
-            return CACHE_SCHEMA[cache_key]
+        task_graph = self.task_graph
         json = {
             "title": "Composite Node configure",
             "type": "object",
@@ -271,7 +270,6 @@ def conf_schema(self):
             "subnodes_conf": {}
         }
         if task_graph:
-            task_graph.build(replace=replacementObj)
 
             def inputNode_fun(inputNode, in_ports):
                 pass
@@ -316,10 +314,9 @@ def outNode_fun(outNode, out_ports):
                         }
                     })
         out_schema = ConfSchema(json=json, ui=ui)
-        CACHE_SCHEMA[cache_key] = out_schema
         return out_schema
 
-    def update_replace(self, replaceObj, task_graph=None):
+    def update_replace(self, replaceObj, task_graph=None, **kwargs):
         # find the other replacment conf
         if 'subnodes_conf' in self.conf:
             for key in self.conf['subnodes_conf'].keys():
@@ -332,7 +329,7 @@ def update_replace(self, replaceObj, task_graph=None):
                     replaceObj[newid].update(self.conf[
                         'subnodes_conf'][key])
 
-    def process(self, inputs):
+    def process(self, inputs, **kwargs):
         """
         Composite computation
 
@@ -345,10 +342,10 @@ def process(self, inputs):
         dataframe
         """
         if 'taskgraph' in self.conf:
-            task_graph = TaskGraph.load_taskgraph(
-                get_file_path(self.conf['taskgraph']))
-            task_graph.build()
-
+            task_graph = self.task_graph
+            # task_graph = TaskGraph.load_taskgraph(
+            #     get_file_path(self.conf['taskgraph']))
+            # task_graph._build()
             outputLists = []
             replaceObj = {}
             input_feeders = []
@@ -356,7 +353,7 @@ def process(self, inputs):
             def inputNode_fun(inputNode, in_ports):
                 inports = inputNode.ports_setup().inports
 
-                class InputFeed(Node):
+                class InputFeed(TemplateNodeMixin, Node):
 
                     def meta_setup(self):
                         output = {}
@@ -371,6 +368,9 @@ def ports_setup(self):
                         # it will be something like { input_port: types }
                         return NodePorts(inports={}, outports=inports)
 
+                    def update(self):
+                        TemplateNodeMixin.update(self)
+
                     def conf_schema(self):
                         return ConfSchema()
 
@@ -413,7 +413,7 @@ def outNode_fun(outNode, out_ports):
                                             inputNode_fun, outNode_fun)
 
             task_graph.extend(input_feeders)
-            self.update_replace(replaceObj, task_graph)
+            self.update_replace(replaceObj, task_graph, **kwargs)
             result = task_graph.run(outputLists, replace=replaceObj)
             output = {}
             for key in result.get_keys():
diff --git a/greenflow/greenflow/plugin_nodes/util/contextCompositeNode.py b/greenflow/greenflow/plugin_nodes/util/contextCompositeNode.py
index 60e3e5c1..3ca7bd07 100644
--- a/greenflow/greenflow/plugin_nodes/util/contextCompositeNode.py
+++ b/greenflow/greenflow/plugin_nodes/util/contextCompositeNode.py
@@ -1,7 +1,7 @@
 from .compositeNode import CompositeNode
-from greenflow.dataframe_flow.cache import CACHE_SCHEMA
 from greenflow.dataframe_flow.portsSpecSchema import (ConfSchema,
-                                                   PortsSpecSchema, NodePorts)
+                                                      PortsSpecSchema,
+                                                      NodePorts)
 from .data_obj import ConfData
 from .json_util import parse_config
 from jsonpath_ng import parse
@@ -46,9 +46,10 @@ def meta_setup(self):
     def conf_schema(self):
         # import pdb
         # pdb.set_trace()
-        cache_key, task_graph, replacementObj = self._compute_hash_key()
-        if cache_key in CACHE_SCHEMA:
-            return CACHE_SCHEMA[cache_key]
+        task_graph = self.task_graph
+        # cache_key, task_graph, replacementObj = self._compute_hash_key()
+        # if cache_key in CACHE_SCHEMA:
+        #     return CACHE_SCHEMA[cache_key]
         json = {
             "title": "Context Composite Node configure",
             "type": "object",
@@ -102,7 +103,7 @@ def conf_schema(self):
                 }
             # import pdb
             # pdb.set_trace()
-            all_fields = parse_config(replacementObj)
+            all_fields = parse_config(self.replacementObj)
             types = list(all_fields.keys())
             addional = json['properties']['context']['additionalProperties']
             addional['properties']['type']['enum'] = types
@@ -174,7 +175,6 @@ def conf_schema(self):
             typelist.append(obj_temp)
 
         if 'taskgraph' in self.conf:
-            task_graph.build(replace=replacementObj)
 
             def inputNode_fun(inputNode, in_ports):
                 pass
@@ -201,16 +201,18 @@ def outNode_fun(outNode, out_ports):
             json['properties']['input']['items']['enum'] = in_ports
             json['properties']['output']['items']['enum'] = out_ports
         out_schema = ConfSchema(json=json, ui=ui)
-        CACHE_SCHEMA[cache_key] = out_schema
         return out_schema
 
     def conf_update(self):
-        input_meta = self.get_input_meta()
-        if self.INPUT_CONFIG in input_meta:
-            if input_meta[self.INPUT_CONFIG]:
-                self.conf.update(input_meta[self.INPUT_CONFIG])
-
-    def update_replace(self, replaceObj, task_graph):
+        input_conf = self.get_input_meta(self.INPUT_CONFIG)
+        if input_conf is not None:
+            self.conf.update(input_conf)
+
+    def update_replace(self, replaceObj, task_graph, **kwargs):
+        """
+        called inside the self.process function to get the updated
+        replacement object before taskgraph.run.
+        """
         # find the other replacment conf
         if task_graph:
             for task in task_graph:
diff --git a/greenflow/setup.py b/greenflow/setup.py
index 9e03054f..b1ce93aa 100644
--- a/greenflow/setup.py
+++ b/greenflow/setup.py
@@ -14,7 +14,7 @@
 
 setup(
     name='greenflow',
-    version='1.0.2',
+    version='1.0.3',
     description='greenflow - RAPIDS Financial Services Algorithms',
     long_description=long_description,
     long_description_content_type='text/markdown',
diff --git a/greenflowlab/README.md b/greenflowlab/README.md
index 69909daf..4aa00376 100644
--- a/greenflowlab/README.md
+++ b/greenflowlab/README.md
@@ -21,18 +21,6 @@ for the server extension and a NPM package named `greenflowlab`
 for the frontend extension.
 
 
-## Build the dev container
-
-In the greenflow root direction
-```bash
-bash docker/build.sh
-```
-Launch your development container, make sure mounting your greenflow directory
-to the container and open `8888` ports.
-
-Set the greenflow path as the folder to start the development or you can open the 
-`workspace.code-workspace` file.
-
 ## Requirements
 
 * JupyterLab >= 3.0
diff --git a/greenflowlab/greenflowlab/handlers.py b/greenflowlab/greenflowlab/handlers.py
index a5d26b9e..700b54e2 100644
--- a/greenflowlab/greenflowlab/handlers.py
+++ b/greenflowlab/greenflowlab/handlers.py
@@ -1,12 +1,15 @@
+import os
 import json
+import tornado
 from jupyter_server.base.handlers import APIHandler
 from jupyter_server.utils import url_path_join
-import tornado
 from greenflow.dataframe_flow import TaskGraph
-from .server_utils import (get_nodes, add_nodes)
-import os
 from greenflow.dataframe_flow.taskGraph import add_module_from_base64
-from greenflow.dataframe_flow.task import get_greenflow_config_modules, load_modules
+from greenflow.dataframe_flow.config_nodes_modules import (
+    get_greenflow_config_modules, load_modules)
+from .server_utils import (get_nodes, add_nodes)
+
+
 try:
     # For python 3.8 and later
     import importlib.metadata as importlib_metadata
diff --git a/greenflowlab/greenflowlab/server_utils.py b/greenflowlab/greenflowlab/server_utils.py
index fa77e4b4..fb13ec8c 100644
--- a/greenflowlab/greenflowlab/server_utils.py
+++ b/greenflowlab/greenflowlab/server_utils.py
@@ -1,12 +1,14 @@
+import inspect
+import uuid
 from greenflow.dataframe_flow import TaskGraph
 from greenflow.dataframe_flow import Node
 from greenflow.dataframe_flow.task import Task
 from greenflow.dataframe_flow._node_flow import OUTPUT_TYPE, OUTPUT_ID
-from greenflow.dataframe_flow import TaskSpecSchema
-from greenflow.dataframe_flow.task import load_modules, get_greenflow_config_modules
+from greenflow.dataframe_flow import (TaskSpecSchema, PortsSpecSchema)
+from greenflow.dataframe_flow.config_nodes_modules import (
+    load_modules, get_greenflow_config_modules, get_node_tgraphmixin_instance)
 import greenflow.plugin_nodes as plugin_nodes
-import inspect
-import uuid
+
 try:
     # For python 3.8 and later
     import importlib.metadata as importlib_metadata
@@ -15,15 +17,18 @@
     import importlib_metadata
 from pathlib import Path
 
-dynamic_modules = {}
+DYNAMIC_MODULES = {}
 
 
 def register_node(module, classObj):
-    if module not in dynamic_modules:
+    if module not in DYNAMIC_MODULES:
         container = {}
-        dynamic_modules[module] = container
+        DYNAMIC_MODULES[module] = container
     else:
-        container = dynamic_modules[module]
+        container = DYNAMIC_MODULES[module]
+
+    # Snippet below is important so that dynamic modules appear under the
+    # "Add Nodes" menu.
     key = classObj.__name__
     container[key] = classObj
 
@@ -41,13 +46,17 @@ def _format_port(port):
     list
         a list of ports with name and type
     """
+    dynamic = PortsSpecSchema.dynamic
+
     all_ports = []
     for key in port:
         one_port = {}
         one_port['name'] = key
-        if 'dynamic' in port[key]:
-            one_port['dynamic'] = port[key]['dynamic']
-        port_type = port[key]['type']
+
+        if dynamic in port[key]:
+            one_port[dynamic] = port[key][dynamic]
+
+        port_type = port[key][PortsSpecSchema.port_type]
         if isinstance(port_type, list):
             types = []
             for t in port_type:
@@ -95,9 +104,8 @@ def get_nodes(task_graph):
     nodes = []
     edges = []
     for task in task_graph:
-        # node = task.get_node_obj()
         node = task_graph[task[TaskSpecSchema.task_id]]
-        out_node = get_node_obj(node)
+        out_node = get_labnode_obj(node)
         connection_inputs = task.get('inputs')
         nodes.append(out_node)
         # out_node['output_meta'] = task_graph[node.uid].output_meta
@@ -113,11 +121,19 @@ def get_nodes(task_graph):
                 num = max(int(port[2:]), num)
             inputs.append({'name': 'in'+str(num+1), "type": [["any"]]})
             out_node['inputs'] = inputs
+
     task_graph.run_cleanup()
+
     return {'nodes': nodes, 'edges': edges}
 
 
-def get_node_obj(node, count_id=True):
+def init_get_labnode_obj(node, count_id=True):
+    node.init()
+    node.update()
+    return get_labnode_obj(node, count_id=count_id)
+
+
+def get_labnode_obj(node, count_id=True):
     """
     It is a private function to convert a Node instance into a dictionary for
     client to consume.
@@ -144,6 +160,7 @@ def get_node_obj(node, count_id=True):
             width = max(max(len(node.uid), len(typeName)) * 10, 100)
         else:
             width = max(len(typeName) * 10, 100)
+
     conf = node._task_obj.get('conf')
     out_node = {'width': width,
                 'id': node.uid,
@@ -154,10 +171,13 @@ def get_node_obj(node, count_id=True):
                 'inputs': _format_port(ports.inports),
                 'outputs': _format_port(ports.outports)}
     out_node['output_meta'] = metadata.outports
+
     if node._task_obj.get('filepath'):
         out_node['filepath'] = node._task_obj.get('filepath')
+
     if node._task_obj.get('module'):
         out_node['module'] = node._task_obj.get('module')
+
     out_node['required'] = metadata.inports
     return out_node
 
@@ -208,12 +228,12 @@ def add_nodes():
     """
     loaded_node_classes = []
     all_modules = get_greenflow_config_modules()
-    print(all_modules)
+    # print('Greenflow config modules: {}\n'.format(all_modules))
     all_nodes = {}
     # not implemented yet for greenflow
     for item in inspect.getmembers(plugin_nodes):
         if inspect.ismodule(item[1]):
-            print(item)
+            # print('Greenflow builtin plugin: {}'.format(item))
             labmod_pkg = 'greenflow.{}'.format(item[0])
             all_nodes[labmod_pkg] = []
             for node in inspect.getmembers(item[1]):
@@ -229,9 +249,9 @@ def add_nodes():
                         'type': node[0],
                         'conf': {},
                         'inputs': []}
-                t = Task(task)
-                n = nodecls(t)
-                nodeObj = get_node_obj(n, False)
+                task_inst = Task(task)
+                node_inst = get_node_tgraphmixin_instance(nodecls, task_inst)
+                nodeObj = init_get_labnode_obj(node_inst, False)
                 all_nodes[labmod_pkg].append(nodeObj)
                 loaded_node_classes.append(nodecls)
 
@@ -261,9 +281,9 @@ def add_nodes():
                     'inputs': [],
                     'module': module
                     }
-            t = Task(task)
-            n = nodecls(t)
-            nodeObj = get_node_obj(n, False)
+            task_inst = Task(task)
+            node_inst = get_node_tgraphmixin_instance(nodecls, task_inst)
+            nodeObj = init_get_labnode_obj(node_inst, False)
             if module_file_or_path.is_dir():
                 # submod = nodecls.__module__.split('.')[1:]
                 # flatten out the namespace hierarchy
@@ -275,27 +295,34 @@ def add_nodes():
                 all_nodes.setdefault(modulename, []).append(nodeObj)
 
             loaded_node_classes.append(nodecls)
-    for module in dynamic_modules.keys():
+
+    for module in DYNAMIC_MODULES.keys():
         modulename = module
-        node_lists = []
-        all_nodes[modulename] = node_lists
-        for class_name in dynamic_modules[module].keys():
-            classObj = dynamic_modules[module][class_name]
-            if issubclass(classObj, Node):
-                task = {'id': 'node_'+str(uuid.uuid4()),
-                        'type': classObj.__name__,
-                        'conf': {},
-                        'inputs': [],
-                        'module': module
-                        }
-                t = Task(task)
-                n = classObj(t)
-                nodeObj = get_node_obj(n, False)
-                node_lists.append(nodeObj)
+        all_nodes[modulename] = []
+        for class_name in DYNAMIC_MODULES[module].keys():
+            nodecls = DYNAMIC_MODULES[module][class_name]
+
+            if not issubclass(nodecls, Node):
+                continue
+
+            if nodecls in loaded_node_classes:
+                continue
+
+            task = {'id': 'node_'+str(uuid.uuid4()),
+                    'type': nodecls.__name__,
+                    'conf': {},
+                    'inputs': [],
+                    'module': module
+                    }
+            task_inst = Task(task)
+            node_inst = get_node_tgraphmixin_instance(nodecls, task_inst)
+            nodeObj = init_get_labnode_obj(node_inst, False)
+            all_nodes.setdefault(modulename, []).append(nodeObj)
+            loaded_node_classes.append(nodecls)
 
     # load all the plugins from entry points
-    for entry_point in importlib_metadata.entry_points().get('greenflow.plugin',
-                                                             ()):
+    for entry_point in importlib_metadata.entry_points().get(
+            'greenflow.plugin', ()):
         mod = entry_point.load()
         modulename = entry_point.name
 
@@ -318,9 +345,9 @@ def add_nodes():
                     'inputs': [],
                     'module': modulename
                     }
-            t = Task(task)
-            n = nodecls(t)
-            nodeObj = get_node_obj(n, False)
+            task_inst = Task(task)
+            node_inst = get_node_tgraphmixin_instance(nodecls, task_inst)
+            nodeObj = init_get_labnode_obj(node_inst, False)
             all_nodes.setdefault(modulename, []).append(nodeObj)
             loaded_node_classes.append(nodecls)
 
diff --git a/greenflowlab/package.json b/greenflowlab/package.json
index da86fc29..d57356fc 100644
--- a/greenflowlab/package.json
+++ b/greenflowlab/package.json
@@ -1,6 +1,6 @@
 {
   "name": "greenflowlab",
-  "version": "1.0.0",
+  "version": "1.0.3",
   "description": "greenflow Jupyterlab extension",
   "keywords": [
     "jupyter",
diff --git a/greenflowlab/setup.py b/greenflowlab/setup.py
index 20986517..d15911c8 100644
--- a/greenflowlab/setup.py
+++ b/greenflowlab/setup.py
@@ -73,6 +73,7 @@
     install_requires=[
         "jupyterlab>=3.0.0rc13,==3.*",
         "ipywidgets",
+        "greenflow"
     ],
     zip_safe=False,
     include_package_data=True,
@@ -81,7 +82,6 @@
     platforms="Linux, Mac OS X, Windows",
     keywords=["Jupyter", "JupyterLab", "JupyterLab3"],
     classifiers=[
-        "License :: OSI Approved :: Apache",
         "Programming Language :: Python",
         "Programming Language :: Python :: 3",
         "Programming Language :: Python :: 3.6",


Test patch:
diff --git a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/backtest/simpleBackTest.py b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/backtest/simpleBackTest.py
index da2c9a84..441247f0 100644
--- a/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/backtest/simpleBackTest.py
+++ b/gQuant/plugins/gquant_plugin/greenflow_gquant_plugin/backtest/simpleBackTest.py
@@ -1,24 +1,50 @@
-from greenflow.dataframe_flow import Node
-from .._port_type_node import _PortTypesMixin
-from greenflow.dataframe_flow.portsSpecSchema import ConfSchema
+from greenflow.dataframe_flow import (
+    Node, PortsSpecSchema, ConfSchema, MetaDataSchema)
+from greenflow.dataframe_flow.template_node_mixin import TemplateNodeMixin
+from ..node_hdf_cache import NodeHDFCacheMixin
 
 
-class SimpleBackTestNode(Node, _PortTypesMixin):
+class SimpleBackTestNode(TemplateNodeMixin, NodeHDFCacheMixin, Node):
 
     def init(self):
-        _PortTypesMixin.init(self)
+        TemplateNodeMixin.init(self)
         self.INPUT_PORT_NAME = 'bardata_in'
         self.OUTPUT_PORT_NAME = 'backtest_out'
-
-    def meta_setup(self):
+        port_type = PortsSpecSchema.port_type
+        port_inports = {
+            self.INPUT_PORT_NAME: {
+                port_type: [
+                    "pandas.DataFrame", "cudf.DataFrame",
+                    "dask_cudf.DataFrame", "dask.dataframe.DataFrame"
+                ]
+            },
+        }
+        port_outports = {
+            self.OUTPUT_PORT_NAME: {
+                port_type: "${port:bardata_in}"
+            }
+        }
         cols_required = {"signal": "float64",
                          "returns": "float64"}
         addition = {"strategy_returns": "float64"}
-        return _PortTypesMixin.addition_meta_setup(self, addition,
-                                                   required=cols_required)
-
-    def ports_setup(self):
-        return _PortTypesMixin.ports_setup(self)
+        meta_inports = {
+            self.INPUT_PORT_NAME: cols_required
+        }
+        meta_outports = {
+            self.OUTPUT_PORT_NAME: {
+                MetaDataSchema.META_OP: MetaDataSchema.META_OP_ADDITION,
+                MetaDataSchema.META_REF_INPUT: self.INPUT_PORT_NAME,
+                MetaDataSchema.META_DATA: addition
+            }
+        }
+        self.template_ports_setup(
+            in_ports=port_inports,
+            out_ports=port_outports
+        )
+        self.template_meta_setup(
+            in_ports=meta_inports,
+            out_ports=meta_outports
+        )
 
     def conf_schema(self):
         json = {
diff --git a/gQuant/plugins/gquant_plugin/tests/unit/test_indicator_node.py b/gQuant/plugins/gquant_plugin/tests/unit/test_indicator_node.py
index dd0a01fe..39ce78ed 100644
--- a/gQuant/plugins/gquant_plugin/tests/unit/test_indicator_node.py
+++ b/gQuant/plugins/gquant_plugin/tests/unit/test_indicator_node.py
@@ -23,6 +23,8 @@
 import unittest
 import cudf
 import greenflow_gquant_plugin.cuindicator as gi
+from greenflow.dataframe_flow.config_nodes_modules import \
+    get_node_tgraphmixin_instance
 from greenflow_gquant_plugin.transform.indicatorNode import IndicatorNode
 from greenflow.dataframe_flow.task import Task
 from .utils import make_orderer
@@ -85,8 +87,10 @@ def test_colums(self):
                     "type": "IndicatorNode",
                     "conf": self.conf,
                     "inputs": {}}
+
         task = Task(node_obj)
-        inN = IndicatorNode(task)
+        inN = get_node_tgraphmixin_instance(IndicatorNode, task)
+        # inN = IndicatorNode(task)
         out_cols = inN.meta_setup().outports
 
         col = "indicator"
diff --git a/gQuant/plugins/gquant_plugin/tests/unit/test_performance.py b/gQuant/plugins/gquant_plugin/tests/unit/test_performance.py
new file mode 100644
index 00000000..5841d221
--- /dev/null
+++ b/gQuant/plugins/gquant_plugin/tests/unit/test_performance.py
@@ -0,0 +1,113 @@
+'''
+Performance Unit Tests
+
+To run unittests:
+
+# Using standard library unittest
+
+python -m unittest -v
+python -m unittest tests/unit/test_performance.py -v
+
+or
+
+python -m unittest discover <test_directory>
+python -m unittest discover -s <directory> -p 'test_*.py'
+
+# Using pytest
+# "conda install pytest" or "pip install pytest"
+pytest -v tests
+pytest -v tests/unit/test_performance.py
+
+'''
+
+import unittest
+from greenflow import TaskGraph
+from .utils import make_orderer
+import cProfile
+import pstats
+import warnings
+import os
+
+ordered, compare = make_orderer()
+unittest.defaultTestLoader.sortTestMethodsUsing = compare
+
+
+class TestPerformance(unittest.TestCase):
+    '''Profile calls to ports_setup and meta_setup.'''
+
+    def setUp(self):
+        warnings.filterwarnings('ignore', message='numpy.ufunc size changed')
+        dirnamefn = os.path.dirname
+        topdir = dirnamefn(dirnamefn(dirnamefn(os.path.realpath(__file__))))
+        os.environ['MODULEPATH'] = str(topdir) + '/modules'
+        os.environ['GREENFLOW_CONFIG'] = str(topdir) + '/greenflowrc'
+
+        self.ports_setup_ref = {
+            'ports_setup.compositeNode.py': 4,
+            'ports_setup.classificationGenerator.py': 2,
+            'ports_setup.csvStockLoader.py': 3,
+            'ports_setup.taskGraph.py': 5,
+            'ports_setup._node_flow.py': 304,
+            'ports_setup.template_node_mixin.py': 61,
+            'ports_setup_ext._node_taskgraph_extension_mixin.py': 61
+        }
+
+        self.meta_setup_ref = {
+            'meta_setup.normalizationNode.py': 2,
+            'meta_setup.compositeNode.py': 4,
+            'meta_setup.classificationGenerator.py': 2,
+            'meta_setup.simpleBackTest.py': 2,
+            'meta_setup.csvStockLoader.py': 3,
+            'meta_setup.taskGraph.py': 5,
+            'meta_setup.node.py': 5,
+            'meta_setup._node_flow.py': 158,
+            'meta_setup.template_node_mixin.py': 47,
+            'meta_setup_ext._node_taskgraph_extension_mixin.py': 47,
+        }
+
+        tgraphpath = str(topdir) + \
+            '/taskgraphs/xgboost_example/xgboost_stock.gq.yaml'
+        profiler = cProfile.Profile()
+        profiler.enable()
+        graph = TaskGraph.load_taskgraph(tgraphpath)
+        graph.build()
+        profiler.disable()
+        stats = pstats.Stats(profiler).sort_stats('ncalls')
+
+        self.stats = stats
+
+    def tearDown(self):
+        pass
+
+    @ordered
+    def test_ports_setup_performance(self):
+        stats = self.stats
+        statkeys = self.stats.stats.keys()
+        keys = [k for k in statkeys if k[-1] in ('ports_setup',)] + \
+            [k for k in statkeys if k[-1] in ('ports_setup_ext',)]
+        for key in keys:
+            dict_key = key[-1]+'.'+key[0].split('/')[-1]
+            msg = "{}.{} is called {} (expected {}) times.".format(
+                key[0].split('/')[-1].split('.')[0], key[-1],
+                stats.stats[key][0], self.ports_setup_ref[dict_key])
+            self.assertTrue(
+                stats.stats[key][0] == self.ports_setup_ref[dict_key], msg)
+
+    @ordered
+    def test_meta_setup_performance(self):
+        stats = self.stats
+        statkeys = self.stats.stats.keys()
+
+        keys = [k for k in statkeys if k[-1] in ('meta_setup',)] + \
+            [k for k in statkeys if k[-1] in ('meta_setup_ext',)]
+        for key in keys:
+            dict_key = key[-1] + '.' + key[0].split('/')[-1]
+            msg = "{}.{} is called {} (expected {}) times.".format(
+                key[0].split('/')[-1].split('.')[0], key[-1],
+                stats.stats[key][0], self.meta_setup_ref[dict_key])
+            self.assertTrue(
+                stats.stats[key][0] == self.meta_setup_ref[dict_key], msg)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/greenflow/tests/unit/custom_port_nodes.py b/greenflow/tests/unit/custom_port_nodes.py
index c95855ed..2117dee1 100644
--- a/greenflow/tests/unit/custom_port_nodes.py
+++ b/greenflow/tests/unit/custom_port_nodes.py
@@ -1,6 +1,4 @@
-import math
 import numpy as np
-import dask
 import pandas as pd
 from greenflow.dataframe_flow import Node, MetaData
 from greenflow.dataframe_flow import NodePorts, PortsSpecSchema
@@ -9,7 +7,7 @@
 import warnings
 
 
-class _PortTypesMixin(object):
+class NodeHDFCacheMixin(object):
 
     def load_cache(self, filename=None) -> dict:
         """
@@ -83,7 +81,7 @@ def save_cache(self, output_data: dict):
                 odf.to_hdf(hf, key, format='table', data_columns=True)
 
 
-class PointNode(_PortTypesMixin, Node):
+class PointNode(NodeHDFCacheMixin, Node):
 
     def ports_setup(self):
         input_ports = {}
@@ -143,7 +141,7 @@ def process(self, inputs):
         return output
 
 
-class DistanceNode(_PortTypesMixin, Node):
+class DistanceNode(NodeHDFCacheMixin, Node):
 
     def ports_setup(self):
         port_type = PortsSpecSchema.port_type
diff --git a/greenflow/tests/unit/test_node_api.py b/greenflow/tests/unit/test_node_api.py
index d0c5fdea..74391a27 100644
--- a/greenflow/tests/unit/test_node_api.py
+++ b/greenflow/tests/unit/test_node_api.py
@@ -27,6 +27,7 @@
 from greenflow.dataframe_flow._node import _Node
 from greenflow.dataframe_flow.node import (Node, _PortsMixin)
 from greenflow.dataframe_flow._node_flow import NodeTaskGraphMixin
+from greenflow.dataframe_flow.config_nodes_modules import get_node_obj
 
 from .utils import make_orderer
 
@@ -37,6 +38,7 @@
 class TestNodeAPI(unittest.TestCase):
 
     def setUp(self):
+        os.environ['GREENFLOW_CONFIG'] = ''
         custom_module = '{}/custom_port_nodes.py'.format(
             os.path.dirname(os.path.realpath(__file__)))
 
@@ -85,16 +87,16 @@ def test_node_instantiation(self):
         self.assertEqual(
             err_msg,
             "Can't instantiate abstract class Node with abstract methods "
-            "meta_setup, process")
+            "meta_setup, ports_setup, process")
 
-        points_node = points_task.get_node_obj()
+        points_node = get_node_obj(points_task)
 
         self.assertIsInstance(points_node, _Node)
         self.assertIsInstance(points_node, Node)
         self.assertIsInstance(points_node, _PortsMixin)
         self.assertNotIsInstance(points_node, NodeTaskGraphMixin)
 
-        points_node = points_task.get_node_obj(tgraph_mixin=True)
+        points_node = get_node_obj(points_task, tgraph_mixin=True)
         self.assertIsInstance(points_node, NodeTaskGraphMixin)
 
     @ordered
@@ -103,7 +105,7 @@ def test_node_ports(self):
         and output ports.
         '''
 
-        distance_node = self.distance_task.get_node_obj()
+        distance_node = get_node_obj(self.distance_task)
         iports = distance_node._get_input_ports()
         oports = distance_node._get_output_ports()
 
diff --git a/greenflow/tests/unit/test_node_taskgraph_typechecking.py b/greenflow/tests/unit/test_node_taskgraph_typechecking.py
index 663bbdd7..86ab80a1 100644
--- a/greenflow/tests/unit/test_node_taskgraph_typechecking.py
+++ b/greenflow/tests/unit/test_node_taskgraph_typechecking.py
@@ -21,7 +21,8 @@
 import copy
 import warnings
 
-from greenflow.dataframe_flow import (Node, PortsSpecSchema, NodePorts, MetaData)
+from greenflow.dataframe_flow import (
+    Node, PortsSpecSchema, NodePorts, MetaData)
 from greenflow.dataframe_flow import (TaskSpecSchema, TaskGraph)
 
 from .utils import make_orderer
@@ -89,14 +90,14 @@ class TestNodeTaskGraphTypechecking(unittest.TestCase):
     def setUp(self):
         warnings.simplefilter('ignore', category=DeprecationWarning)
 
-        numgen_spec = {
+        self.numgen_spec = {
             TaskSpecSchema.task_id: 'numgen',
             TaskSpecSchema.node_type: NodeNumGen,
             TaskSpecSchema.conf: {},
             TaskSpecSchema.inputs: {}
         }
 
-        numproc_spec = {
+        self.numproc_spec = {
             TaskSpecSchema.task_id: 'numproc',
             TaskSpecSchema.node_type: NodeNumProc,
             TaskSpecSchema.conf: {},
@@ -105,9 +106,6 @@ def setUp(self):
             }
         }
 
-        self.numgen_spec = numgen_spec
-        self.numproc_spec = numproc_spec
-
     def tearDown(self):
         pass
 
diff --git a/greenflow/tests/unit/test_taskgraph_api.py b/greenflow/tests/unit/test_taskgraph_api.py
index 5cc171b0..2d118afa 100644
--- a/greenflow/tests/unit/test_taskgraph_api.py
+++ b/greenflow/tests/unit/test_taskgraph_api.py
@@ -29,7 +29,7 @@
 import unittest
 import pandas as pd
 from greenflow.dataframe_flow import (TaskSpecSchema, TaskGraph)
-from greenflow.dataframe_flow.task import DEFAULT_MODULE  # noqa: F401
+from greenflow.dataframe_flow.config_nodes_modules import DEFAULT_MODULE
 from greenflow.dataframe_flow import Node
 
 from .utils import make_orderer
@@ -85,7 +85,8 @@ def setUp(self):
 
         # Create a temporary directory
         self._test_dir = tempfile.mkdtemp()
-        os.environ['GREENFLOW_CACHE_DIR'] = os.path.join(self._test_dir, '.cache')
+        os.environ['GREENFLOW_CACHE_DIR'] = os.path.join(self._test_dir,
+                                                         '.cache')
 
     def tearDown(self):
         global DEFAULT_MODULE