Repository: leon-thomm/ryvencore (language: python)

Issue:
Main

dtypes default not used
Hi @leon-thomm, I've been using ryvencore over on [ironflow](https://github.com/pyiron/ironflow) to build a jupyter-based gui to drive for our project [pyiron](pyiron.org) and I'm absolutely loving it. 

I discovered that the default values in the initial input channels were not being used. In addition to the dtypes, where `default` is actively solicited in the initializer, I thought it might be nice to allow specifying defaults for non-typed input using `add_data['val']`.

A MWE example is below with a little fix to put in `NodeInput.__init__` if you'd like. I'd be happy to make a PR, but when I forked the repo I discovered that main here is way out of date compared to the conda-forge installation (v0.3.1.1). `NodeInput.__init__` doesn't even take a `dtypes: Dtypes = None` kwarg. Where is the more up-to-date code coming from? The only branch ahead of main here doesn't have it, and unfortunately I don't have permissions to dig into the [conda feedstock source](https://anaconda.org/conda-forge/ryvencore) to look there.

MWE:
```python
import ryvencore as rc

class MyNode(rc.Node):
    title = 'mwe'
    init_inputs = [
        rc.NodeInputBP(type_='data', add_data={'val': 'towel'}),  # Without dtype
        rc.NodeInputBP(type_='data', dtype=rc.dtypes.Integer(default=42))
    ]
    
    def place_event(self):
        super().place_event()
        print(f"{self.__class__.__name__} got placed")
    
session = rc.Session()
session.register_node(MyNode)
script = session.create_script(title='mwe')
node = script.flow.create_node(MyNode)
print("Initialized?", node.initialized)
print("Values after initialization:", node.input(0), node.input(1))

def set_defaults(node_input):
    """Something like could be added to NodeInput.__init__"""
    if node_input.dtype is not None:
        node_input.update(node_input.dtype.default)
    elif 'val' in node_input.add_data.keys():
        node_input.update(node_input.add_data['val'])
        
set_defaults(node.inputs[0])
set_defaults(node.inputs[1])
print("Values after setting defaults:", node.input(0), node.input(1))

>>> MyNode got placed
>>> Initialized? True
>>> Values after initialization: None None
>>> Values after setting defaults: towel 42
```

edited for hyperlinks



Discussion / hints:

Fix patch:
diff --git a/.github/workflows/build-docs.yml b/.github/workflows/build-docs.yml
new file mode 100644
index 0000000..1f5e938
--- /dev/null
+++ b/.github/workflows/build-docs.yml
@@ -0,0 +1,39 @@
+name: "Pull Request Docs Check"
+on:
+  push:
+    branches:
+      - main
+  pull_request:
+    branches:
+      - main
+
+jobs:
+  docs:
+    runs-on: ubuntu-latest
+    steps:
+    - uses: actions/checkout@v1
+    - name: Set up Python 3.10
+      uses: actions/setup-python@v4
+      with:
+        python-version: 3.10.7
+    - name: Install dependencies
+      run: |
+        python -m pip install --upgrade pip
+        pip install sphinx
+        pip install maisie_sphinx_theme
+        pip install .
+    - name: Build docs
+      run: |
+        cd docs
+        make html
+        cd ..
+    - name: Push docs to gh-pages branch
+      uses: peaceiris/actions-gh-pages@v3
+      with:
+        github_token: ${{ secrets.GITHUB_TOKEN }}
+        publish_dir: ./docs/build/html
+        publish_branch: gh-pages
+        keep_files: false
+        user_name: 'github-actions[bot]'
+        user_email: 'github-actions[bot]@users.noreply.github.com'
+        commit_message: 'Update docs'
\ No newline at end of file
diff --git a/.github/workflows/publish-package.yml b/.github/workflows/publish-package.yml
index 113d0f4..e18ed6c 100644
--- a/.github/workflows/publish-package.yml
+++ b/.github/workflows/publish-package.yml
@@ -1,8 +1,9 @@
 name: Publish package distribution to TestPyPi and PyPi
+# only run when a new tag is created
 on:
-  push:
-    branches:
-      - main
+  create:
+    tags:
+      - '*'
 jobs:
   Build-and-Publish:
     runs-on: ubuntu-latest
diff --git a/README.md b/README.md
index 8a4bf8e..4925565 100644
--- a/README.md
+++ b/README.md
@@ -3,9 +3,9 @@
   <img src="./docs/img/logo.png" alt="drawing" width="70%"/>
 </p>
 
-An experimental Python library for graph-based processing, designed for flow-based/node-based visual scripting editors. While it is the backbone of the [Ryven](https://github.com/leon-thomm/Ryven) project, it can very much be used in other contexts as well.
+An experimental Python library for graph-based processing, designed for flow-based/node-based visual scripting editors. It is the backbone of the [Ryven](https://github.com/leon-thomm/Ryven) project, but it can very much be used in other contexts as well.
 
-While ryvencore is written purely in Python, with not a single dependency it is very lightweight and highly compatible. It can be compiled with Cython, see the `setup_cython.py` file. The performance seems comparable so far, but the code hasn't been optimized for Cython yet, so there might be a lot of potential. Please consider contributing. [Pyodide](https://github.com/pyodide/pyodide) provides a WebAssembly port of ryvencore.
+While ryvencore is written purely in Python, it is very lightweight and highly compatible. It can be compiled with Cython, see the `setup_cython.py` file. The performance seems comparable so far, but the code hasn't been optimized for Cython yet, so there might be a lot of potential. Please consider contributing. [Pyodide](https://github.com/pyodide/pyodide) provides a WebAssembly port of ryvencore.
 
 ### Installation
 
@@ -22,9 +22,11 @@ pip install .
 
 ### Usage
 
-As an experimental library, the API is not fully stable and small breaking changes over time should be expected. There is no maintained usage guide, but the code is documented and auto-generated docs are available [here](https://leon-thomm.github.io/ryvencore/).
+As an experimental library, the API is not fully stable and small breaking changes over time should be expected. Generally, the API is defined by what is included in the [docs](https://leon-thomm.github.io/ryvencore/).
 
-A small example:
+### Examples
+
+**loading a project** e.g. exported from Ryven
 
 ```python
 import ryvencore as rc
@@ -34,24 +36,21 @@ import sys
 if __name__ == '__main__':
     # project file path
     fpath = sys.args[1]
-    
+
     # read project file
     with open(fpath, 'r') as f:
         project: dict = json.loads(f.read())
-    
+
     # run ryvencore
     session = rc.Session()
     session.load(project)
 
-    # now we can access all components, for example:
-    
-    # get the first flow
-    scripts = session.scripts
-    flow1 = scripts[0].flow
+    # access the first flow
+    f = session.flows[0]
     
     # and the last node that was created
-    my_node = flow1.nodes[-1]
-    
+    my_node = f.nodes[-1]
+
     # and execute it
     my_node.update()
 ```
@@ -65,7 +64,7 @@ The main features include
 - **data *and* exec flow support** - unlike lots of other solutions out there, ryvencore supports exec flows
 - **variables system** with subscribe and update mechanism to build nodes that automatically adapt to change of data
 - **built-in logging** based on python's `logging` module
-- **actions system for nodes**
+- **actions system for nodes** (WIP)
 
 ### Licensing
 
diff --git a/docs/source/_static/custom.css b/docs/source/_static/custom.css
index 39512f8..4ae2794 100644
--- a/docs/source/_static/custom.css
+++ b/docs/source/_static/custom.css
@@ -1,3 +1,3 @@
 .content {
-    width: 70em
+    /*width: 70em*/
 }
\ No newline at end of file
diff --git a/docs/source/conf.py b/docs/source/conf.py
index 259aa73..45ad7bb 100644
--- a/docs/source/conf.py
+++ b/docs/source/conf.py
@@ -20,7 +20,7 @@
 # -- Project information -----------------------------------------------------
 
 project = 'ryvencore'
-copyright = '2021, Leon Thomm'
+copyright = '2022, Leon Thomm'
 author = 'Leon Thomm'
 
 # The full version, including alpha/beta/rc tags
@@ -35,6 +35,7 @@
 extensions = [
     'sphinx.ext.autodoc',
     'sphinx.ext.napoleon',
+    'sphinx.ext.viewcode',
 ]
 
 add_module_names = False
@@ -57,7 +58,13 @@
 # a list of builtin themes.
 #
 
-html_theme = 'furo'
+import maisie_sphinx_theme
+extensions.append("maisie_sphinx_theme")
+html_theme = 'maisie_sphinx_theme'
+html_theme_path = maisie_sphinx_theme.html_theme_path()
+
+# html_theme = 'alabaster'
+# 'furo'
 # 'karma_sphinx_theme'
 # 'insegel'
 # 'pydata_sphinx_theme'
diff --git a/docs/source/index.rst b/docs/source/index.rst
index c932865..c29b600 100644
--- a/docs/source/index.rst
+++ b/docs/source/index.rst
@@ -6,17 +6,12 @@
 `ryvencore` |version|
 =====================================
 
+:ref:`genindex`
+
 .. toctree::
    :maxdepth: 2
+   :caption: Contents:
 
    ryvencore
 
-
-
-
-Indices and tables
-==================
-
-* :ref:`genindex`
-* :ref:`modindex`
-* :ref:`search`
+Welcome to the ``ryvencore`` documentation!
diff --git a/docs/source/modules.rst b/docs/source/modules.rst
deleted file mode 100644
index 60a43a5..0000000
--- a/docs/source/modules.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore
-=========
-
-.. toctree::
-   :maxdepth: 2
-
-   ryvencore
diff --git a/docs/source/ryvencore.Base.rst b/docs/source/ryvencore.Base.rst
deleted file mode 100644
index ee9a70b..0000000
--- a/docs/source/ryvencore.Base.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.Base module
-=====================
-
-.. automodule:: ryvencore.Base
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.Connection.rst b/docs/source/ryvencore.Connection.rst
deleted file mode 100644
index 789bf77..0000000
--- a/docs/source/ryvencore.Connection.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.Connection module
-===========================
-
-.. automodule:: ryvencore.Connection
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.Flow.rst b/docs/source/ryvencore.Flow.rst
deleted file mode 100644
index dbd87b1..0000000
--- a/docs/source/ryvencore.Flow.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.Flow module
-=====================
-
-.. automodule:: ryvencore.Flow
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.FlowExecutor.rst b/docs/source/ryvencore.FlowExecutor.rst
deleted file mode 100644
index 7202754..0000000
--- a/docs/source/ryvencore.FlowExecutor.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.FlowExecutor module
-=============================
-
-.. automodule:: ryvencore.FlowExecutor
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.InfoMsgs.rst b/docs/source/ryvencore.InfoMsgs.rst
deleted file mode 100644
index 06a2c1e..0000000
--- a/docs/source/ryvencore.InfoMsgs.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.InfoMsgs module
-=========================
-
-.. automodule:: ryvencore.InfoMsgs
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.Node.rst b/docs/source/ryvencore.Node.rst
deleted file mode 100644
index ff86114..0000000
--- a/docs/source/ryvencore.Node.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.Node module
-=====================
-
-.. automodule:: ryvencore.Node
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.NodePort.rst b/docs/source/ryvencore.NodePort.rst
deleted file mode 100644
index 9248665..0000000
--- a/docs/source/ryvencore.NodePort.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.NodePort module
-=========================
-
-.. automodule:: ryvencore.NodePort
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.NodePortBP.rst b/docs/source/ryvencore.NodePortBP.rst
deleted file mode 100644
index 9a2ef37..0000000
--- a/docs/source/ryvencore.NodePortBP.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.NodePortBP module
-===========================
-
-.. automodule:: ryvencore.NodePortBP
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.RC.rst b/docs/source/ryvencore.RC.rst
deleted file mode 100644
index e71f222..0000000
--- a/docs/source/ryvencore.RC.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.RC module
-===================
-
-.. automodule:: ryvencore.RC
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.Script.rst b/docs/source/ryvencore.Script.rst
deleted file mode 100644
index 4f85d61..0000000
--- a/docs/source/ryvencore.Script.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.Script module
-=======================
-
-.. automodule:: ryvencore.Script
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.Session.rst b/docs/source/ryvencore.Session.rst
deleted file mode 100644
index adf766e..0000000
--- a/docs/source/ryvencore.Session.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.Session module
-========================
-
-.. automodule:: ryvencore.Session
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.addons.default.rst b/docs/source/ryvencore.addons.default.rst
new file mode 100644
index 0000000..a40c792
--- /dev/null
+++ b/docs/source/ryvencore.addons.default.rst
@@ -0,0 +1,29 @@
+ryvencore.addons.default package
+================================
+
+Submodules
+----------
+
+ryvencore.addons.default.Logging module
+---------------------------------------
+
+.. automodule:: ryvencore.addons.default.Logging
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+ryvencore.addons.default.Variables module
+-----------------------------------------
+
+.. automodule:: ryvencore.addons.default.Variables
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+Module contents
+---------------
+
+.. automodule:: ryvencore.addons.default
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/ryvencore.dtypes.rst b/docs/source/ryvencore.addons.rst
similarity index 50%
rename from docs/source/ryvencore.dtypes.rst
rename to docs/source/ryvencore.addons.rst
index d574e47..1d0d403 100644
--- a/docs/source/ryvencore.dtypes.rst
+++ b/docs/source/ryvencore.addons.rst
@@ -1,18 +1,18 @@
-ryvencore.dtypes package
+ryvencore.addons package
 ========================
 
-Submodules
-----------
+Subpackages
+-----------
 
 .. toctree::
-   :maxdepth: 2
+   :maxdepth: 4
 
-   ryvencore.dtypes.dtypes
+   ryvencore.addons.default
 
 Module contents
 ---------------
 
-.. automodule:: ryvencore.dtypes
+.. automodule:: ryvencore.addons
    :members:
    :undoc-members:
    :show-inheritance:
diff --git a/docs/source/ryvencore.dtypes.dtypes.rst b/docs/source/ryvencore.dtypes.dtypes.rst
deleted file mode 100644
index 2be19b2..0000000
--- a/docs/source/ryvencore.dtypes.dtypes.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.dtypes.dtypes module
-==============================
-
-.. automodule:: ryvencore.dtypes.dtypes
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.logging.Logger.rst b/docs/source/ryvencore.logging.Logger.rst
deleted file mode 100644
index bb230c8..0000000
--- a/docs/source/ryvencore.logging.Logger.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.logging.Logger module
-===============================
-
-.. automodule:: ryvencore.logging.Logger
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.logging.LogsManager.rst b/docs/source/ryvencore.logging.LogsManager.rst
deleted file mode 100644
index 5e0fdae..0000000
--- a/docs/source/ryvencore.logging.LogsManager.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.logging.LogsManager module
-====================================
-
-.. automodule:: ryvencore.logging.LogsManager
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.logging.rst b/docs/source/ryvencore.logging.rst
deleted file mode 100644
index 63416a9..0000000
--- a/docs/source/ryvencore.logging.rst
+++ /dev/null
@@ -1,19 +0,0 @@
-ryvencore.logging package
-=========================
-
-Submodules
-----------
-
-.. toctree::
-   :maxdepth: 2
-
-   ryvencore.logging.Logger
-   ryvencore.logging.LogsManager
-
-Module contents
----------------
-
-.. automodule:: ryvencore.logging
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.pkg_info.rst b/docs/source/ryvencore.pkg_info.rst
deleted file mode 100644
index 86c5b66..0000000
--- a/docs/source/ryvencore.pkg_info.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.pkg\_info module
-==========================
-
-.. automodule:: ryvencore.pkg_info
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.rst b/docs/source/ryvencore.rst
index 008e391..e66e1a2 100644
--- a/docs/source/ryvencore.rst
+++ b/docs/source/ryvencore.rst
@@ -5,31 +5,53 @@ Subpackages
 -----------
 
 .. toctree::
-   :maxdepth: 2
+   :maxdepth: 4
 
-   ryvencore.dtypes
-   ryvencore.logging
-   ryvencore.script_variables
+   ryvencore.addons
 
 Submodules
 ----------
 
-.. toctree::
-   :maxdepth: 2
-
-   ryvencore.Base
-   ryvencore.Connection
-   ryvencore.Flow
-   ryvencore.FlowExecutor
-   ryvencore.InfoMsgs
-   ryvencore.Node
-   ryvencore.NodePort
-   ryvencore.NodePortBP
-   ryvencore.RC
-   ryvencore.Script
-   ryvencore.Session
-   ryvencore.pkg_info
-   ryvencore.utils
+ryvencore.AddOn module
+----------------------
+
+.. automodule:: ryvencore.AddOn
+   :members: AddOn
+   :undoc-members:
+   :show-inheritance:
+
+
+ryvencore.Data module
+---------------------
+
+.. automodule:: ryvencore.Data
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+ryvencore.Flow module
+---------------------
+
+.. automodule:: ryvencore.Flow
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+ryvencore.Node module
+---------------------
+
+.. automodule:: ryvencore.Node
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+ryvencore.Session module
+------------------------
+
+.. automodule:: ryvencore.Session
+   :members:
+   :undoc-members:
+   :show-inheritance:
 
 Module contents
 ---------------
diff --git a/docs/source/ryvencore.script_variables.Variable.rst b/docs/source/ryvencore.script_variables.Variable.rst
deleted file mode 100644
index 171d4ac..0000000
--- a/docs/source/ryvencore.script_variables.Variable.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.script\_variables.Variable module
-===========================================
-
-.. automodule:: ryvencore.script_variables.Variable
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.script_variables.VarsManager.rst b/docs/source/ryvencore.script_variables.VarsManager.rst
deleted file mode 100644
index f2201ce..0000000
--- a/docs/source/ryvencore.script_variables.VarsManager.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.script\_variables.VarsManager module
-==============================================
-
-.. automodule:: ryvencore.script_variables.VarsManager
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.script_variables.rst b/docs/source/ryvencore.script_variables.rst
deleted file mode 100644
index b1bd4e0..0000000
--- a/docs/source/ryvencore.script_variables.rst
+++ /dev/null
@@ -1,19 +0,0 @@
-ryvencore.script\_variables package
-===================================
-
-Submodules
-----------
-
-.. toctree::
-   :maxdepth: 2
-
-   ryvencore.script_variables.Variable
-   ryvencore.script_variables.VarsManager
-
-Module contents
----------------
-
-.. automodule:: ryvencore.script_variables
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/docs/source/ryvencore.utils.rst b/docs/source/ryvencore.utils.rst
deleted file mode 100644
index 38cf5d9..0000000
--- a/docs/source/ryvencore.utils.rst
+++ /dev/null
@@ -1,7 +0,0 @@
-ryvencore.utils module
-======================
-
-.. automodule:: ryvencore.utils
-   :members:
-   :undoc-members:
-   :show-inheritance:
diff --git a/reinstall.bat b/reinstall.bat
deleted file mode 100644
index f29b7cc..0000000
--- a/reinstall.bat
+++ /dev/null
@@ -1,5 +0,0 @@
-pip uninstall --yes ryvencore
-rmdir /s /q build
-rmdir /s /q dist
-rmdir /s /q ryvencore.egg-info
-python setup.py install
\ No newline at end of file
diff --git a/ryvencore/AddOn.py b/ryvencore/AddOn.py
new file mode 100644
index 0000000..3da1f70
--- /dev/null
+++ b/ryvencore/AddOn.py
@@ -0,0 +1,92 @@
+"""
+*ALPHA*
+
+This module defines a simple add-on system to extend ryvencore's functionalities.
+Some default add-ons are provided in the addons.default package, and additional add-ons
+can be added and registered in the Session.
+
+An add-on
+    - has a name and a version
+    - is session-local, not flow-local (but you can of course implement per-flow functionality)
+    - manages its own state (in particular ``get_state()`` and ``set_state()``)
+    - can store additional node-specific data in the node's ``data`` dict when it's serialized
+    - will be accessible through the nodes API: ``self.get_addon('your_addon')`` in your nodes
+
+Add-on access is blocked during loading (deserialization), so nodes should not access any
+add-ons during the execution of ``Node.__init__`` or ``Node.set_data``.
+This prevents inconsistent states. Nodes are loaded first, then the add-ons. 
+Therefore, the add-on should be sufficiently isolated and self-contained.
+
+To define a custom add-on:
+    - create a directory ``your_addons`` for you addons or use ryvencore's addon directory
+    - create a module for your addon ``YourAddon.py`` in ``your_addons``
+    - create a class ``YourAddon(ryvencore.AddOn)`` that defines your add-on's functionality
+    - instantiate it into a top-level variable: ``addon = YourAddon()`` at the end of the module
+    - register your addon directory in the Session: ``session.register_addon_dir('path/to/your_addons')``
+
+See ``ryvencore.addons.default`` for examples.
+"""
+
+from ryvencore.Base import Base
+
+
+class AddOn(Base):
+    name = ''
+    version = ''
+
+    def register(self, session):
+        """
+        Called when the add-on is registered with a session.
+        """
+        self.session = session
+
+    def _on_node_created(self, flow, node):
+        """
+        Called when a node is created. This happens only once, whereas
+        a node can be added and removed multiple times, see
+        on_node_added() and
+        on_node_removed().
+        """
+        pass
+
+    def _on_node_added(self, flow, node):
+        """
+        Called when a node is added to a flow.
+        """
+        pass
+
+    def _on_node_removed(self, flow, node):
+        """
+        Called when a node is removed from a flow.
+        """
+        pass
+
+    def _extend_node_data(self, node, data: dict):
+        """
+        Extend the node data dict with additional add-on-related data.
+        """
+        pass
+
+    # def _extend_flow_data(self, flow, data: dict):
+    #     """
+    #     Extend the flow data dict with additional add-on-related data.
+    #     """
+    #     pass
+    #
+    # def _extend_session_data(self, data: dict):
+    #     """
+    #     Extend the session data dict with additional add-on-related data.
+    #     """
+    #     pass
+
+    def get_state(self) -> dict:
+        """
+        Return the state of the add-on as JSON-compatible a dict.
+        """
+        return {}
+
+    def set_state(self, state: dict):
+        """
+        Set the state of the add-on from the dict generated in get_state().
+        """
+        pass
diff --git a/ryvencore/Base.py b/ryvencore/Base.py
index b003d76..c491f62 100644
--- a/ryvencore/Base.py
+++ b/ryvencore/Base.py
@@ -1,16 +1,12 @@
 """
 This module defines features and behavior that applies to most internal
-components. It defines the Base (parent) class featuring events, automatic and
-custom ID assignments, serialization, and customizable extension of serialization.
+components. It defines the Base (parent) class.
 """
 
 
 def complete_data(data: dict) -> dict:
     """
-    Default implementation for supplementing data with additional (e.g. frontend)
-    information. For example, a frontend can store additional information like
-    current position, color of a node, etc. by replacing this function by an
-    according custom handler.
+    See ``Base.complete_data_function``.
     """
     return data
 
@@ -44,88 +40,73 @@ def emit(self, *args):
         for cb in self._slots:
             cb(*args)
 
-
-class Base:
+class IDCtr:
     """
-    Base class for all abstract components. Provides functionality for ID counting.
-    Assigns a global ID to every object and provides an optional custom ID counter for additional custom counting.
+    A simple ascending integer ID counter.
+    Guarantees uniqueness during lifetime or the program (not only of the Session).
+    This approach is preferred over UUIDs because UUIDs need a networking context
+    and require according system support which might not be available everywhere.
     """
 
-    class IDCtr:
-        """
-        A simple ascending integer counter.
-        """
+    def __init__(self):
+        self.ctr = -1
 
-        def __init__(self):
-            self.ctr = -1
+    def count(self):
+        """increases the counter and returns the new count. first time is 0"""
+        self.ctr += 1
+        return self.ctr
 
-        def count(self):
-            """increases the counter and returns the new count. first time is 0"""
-            self.ctr += 1
-            return self.ctr
+    def set_count(self, cnt):
+        if cnt < self.ctr:
+            raise Exception("Decreasing ID counters is illegal")
+        else:
+            self.ctr = cnt
 
-        def set_count(self, cnt):
-            if cnt < self.ctr:
-                raise Exception("Decreasing ID counters is illegal")
-            else:
-                self.ctr = cnt
+class Base:
+    """
+    Base class for all abstract components. It provides:
+
+    Functionality for ID counting:
+        - an automatic ``GLOBAL_ID`` unique during the lifetime of the program
+        - a ``PREV_GLOBAL_ID`` for re-identification after save & load,
+          automatically set in ``load()``
+
+    Serialization:
+        - the ``data()`` method gets reimplemented by subclasses to serialize
+        - the ``load()`` method gets reimplemented by subclasses to deserialize
+        - the static attribute ``Base.complete_data_function`` can be set to
+          a function which extends the data dict of any component with additional
+          information, which is useful e.g. in a frontend context
+    """
 
     # all abstract components have a global ID
-    global_id_ctr = IDCtr()
-
-    # optional custom ID counter
-    id_ctr = None
-    # notice that the attribute is static, but a subclass changing it will not change it for Base
-    # and hence not for other Base subclasses, only for itself
-
-    # events
-    events = {}
-    # format: {event_name : tuple_of_argument_types}
-    # the arguments tuple only serves documentation purposes
-
-    def __init__(self):
-        self.GLOBAL_ID = self.global_id_ctr.count()
-
-        # ignore custom ID if it has already been set
-        if self.id_ctr is not None and not (hasattr(self, 'ID') and self.ID is not None):
-            self.ID = self.id_ctr.count()
+    _global_id_ctr = IDCtr()
 
-        self._slots = {
-            ev: []
-            for ev in self.events
-        }
+    # TODO: this produces a memory leak, because the objects are never removed
+    #  from the dict. It shouldn't be a problem as long as PREF_GLOBAL_ID is
+    #  only used for loading.
+    _prev_id_objs = {}
 
-    """
-    
-    CUSTOM DATA
-    
-    """
+    @classmethod
+    def obj_from_prev_id(cls, prev_id: int):
+        """returns the object with the given previous id"""
+        return cls._prev_id_objs.get(prev_id)
 
-    # this can be conveniently set to another function by the host to implement
-    # adding additional (e.g. frontend-related) information to the data dict
     complete_data_function = complete_data
 
+    def __init__(self):
+        self.GLOBAL_ID = self._global_id_ctr.count()
+        self.PREV_GLOBAL_ID = None
+
     def data(self) -> dict:
         """converts the object to a JSON compatible dict for serialization"""
-        return None
+        return {'GID': self.GLOBAL_ID}
 
     def complete_data(self, data: dict) -> data:
         return Base.complete_data_function(data)
 
-    """
-    
-    EVENTS
-    
-    """
-
-    def on(self, ev: Event, callback):
-        # self._slots[ev].append(callback)
-        ev.connect(callback)
-
-    def off(self, ev: Event, callback):
-        # self._slots[ev].remove(callback)
-        ev.disconnect(callback)
-
-    # def _emit(self, ev: str, *args, **kwargs):
-    #     for s in self._slots[ev]:
-    #         s(args, kwargs)
+    def load(self, data: dict):
+        """recreate the object from the data dict returned by ``data()``"""
+        if dict is not None:
+            self.PREV_GLOBAL_ID = data['GID']
+            self._prev_id_objs[self.PREV_GLOBAL_ID] = self
diff --git a/ryvencore/Connection.py b/ryvencore/Connection.py
deleted file mode 100644
index 2a5d5fc..0000000
--- a/ryvencore/Connection.py
+++ /dev/null
@@ -1,33 +0,0 @@
-"""
-deprecated and now unused
-"""
-
-from .Base import Base, Event
-
-from .InfoMsgs import InfoMsgs
-
-
-class Connection(Base):
-    """
-    The base class for both types of connections. All data is transmitted through a connection from an output
-    port to some connected input port.
-    """
-
-    def __init__(self, params):
-        Base.__init__(self)
-
-        self.activated = Event(object)  # TODO: connections are not activated anymore
-
-        self.out, self.inp, self.flow = params
-
-
-class ExecConnection(Connection):
-    pass
-
-
-class DataConnection(Connection):
-
-    def __init__(self, params):
-        super().__init__(params)
-
-        self.data = None
diff --git a/ryvencore/Data.py b/ryvencore/Data.py
index 050877a..48db0ff 100644
--- a/ryvencore/Data.py
+++ b/ryvencore/Data.py
@@ -1,46 +1,100 @@
 """
-This file defines the Data type, which must be used to pass data between nodes.
-It is subclassed by the nodes to define the data type they work with.
-It's primary purpose is to provide serialization and deserialization methods of the data.
-Refer to my notes about graph state and serialization.
+This file defines the ``Data`` type, which must be used to pass data between nodes.
+It should be subclassed to define custom data types. In particular, serialization
+and deserialization must be implemented for each respective type. Types that are
+pickle serializable by default can be used directly with ``Data(my_data)``.
 """
 
 
 class Data:
     """
     Base class for data objects.
-    Subclass this class to send data to other nodes. You can directly extend the class
-    to provide a common interface for your types, but you must implement serialization
-    and deserialization accordingly.
-    As data that is ready for sharing between nodes through the graph is part of the
-    state of the graph, it must be serializable and deserializable.
+
+    Subclass this class and implement serialization and deserialization accordingly
+    to send data to other nodes.
 
     In case of large data sets being shared, you might want to leave serialization
     empty, which means the graph will not enter the same state when you reload it,
     which is fine as long as your nodes are built appropriately e.g. such that you can
     quickly regenerate that state by updating the root node.
 
-    Be careful with nodes built for sharing complex output data over multiple outputs.
-    Refer to python's object referencing rules. In particular, if you share data object
-    D with successor nodes N1 and N2, and N1 changes D, then N2 will also see the change.
-    To avoid this you might want to make sure to copy D once it's consumed for the second
-    time, which you can also conveniently implement in your Data class.
+    Be careful when consuming complex input data: modification can lead to undesired
+    effects. In particular, if you share some data object ``D`` with successor nodes
+    ``N1`` and ``N2``, and ``N1`` changes ``D`` directly, then ``N2``
+    will see the change as well, because they look at the same Data object:
+
+    >>> import ryvencore as rc
+    >>>
+    >>> class Producer(rc.Node):
+    ...     init_outputs = [rc.NodeOutputType()]
+    ...
+    ...     def push_data(self, D):
+    ...         self.D = D
+    ...         self.update()
+    ...
+    ...     def update_event(self, inp=-1):
+    ...         self.set_output_val(0, self.D)
+    >>>
+    >>> class Consumer(rc.Node):
+    ...     init_inputs = [rc.NodeInputType()]
+    ...
+    ...     def update_event(self, inp=-1):
+    ...         p = self.input(0).payload
+    ...         p.append(4)
+    ...         print(p)
+    >>>
+    >>> def build_and_run(D):
+    ...     s = rc.Session()
+    ...     f = s.create_flow('main')
+    ...     producer =  f.create_node(Producer)
+    ...     consumer1 = f.create_node(Consumer)
+    ...     consumer2 = f.create_node(Consumer)
+    ...     f.connect_nodes(producer.outputs[0], consumer1.inputs[0])
+    ...     f.connect_nodes(producer.outputs[0], consumer2.inputs[0])
+    ...     producer.push_data(D)
+    >>>
+    >>> build_and_run(rc.Data([1, 2, 3]))
+    [1, 2, 3, 4]
+    [1, 2, 3, 4, 4]
+
+    This can be useful for optimization when sharing large data, but might not
+    be what you want.
+    To avoid this you might want to make sure to copy ``D`` when its payload is
+    consumed:
+
+    >>> class MyListData(rc.Data):
+    ...     @property
+    ...     def payload(self):
+    ...         return self._payload.copy()
+    >>>
+    >>> build_and_run(MyListData([1, 2, 3]))
+    [1, 2, 3, 4]
+    [1, 2, 3, 4]
     """
 
-    def __init__(self, value=None, deserialize_from=None):
-        if deserialize_from is not None:
-            self.deserialize(deserialize_from)
+    def __init__(self, value=None, load_from=None):
+        if load_from is not None:
+            self.set_data(load_from)
         else:
-            self.value = value
+            self._payload = value
+
+    @property
+    def payload(self):
+        return self._payload
+
+    @payload.setter
+    def payload(self, value):
+        self._payload = value
 
-    def serialize(self) -> dict:
+    def get_data(self):
         """
-        Serialize the data object to a JSON-compatible dict.
+        Transform the data object to a ``pickle`` serializable object.
+        DO NOT use this function to access the payload, use ``payload`` instead.
         """
-        return {'value': self.value}
+        return self.payload
 
-    def deserialize(self, data):
+    def set_data(self, data):
         """
-        Deserialize the data object from a dict.
+        Deserialize the data object from the dict created in ``get_data()``.
         """
-        self.value = data['value']
+        self.payload = data
diff --git a/ryvencore/Flow.py b/ryvencore/Flow.py
index fbc615a..533df47 100644
--- a/ryvencore/Flow.py
+++ b/ryvencore/Flow.py
@@ -1,26 +1,118 @@
+"""
+This module defines the abstract flow, managing node, edges, etc.
+Flow execution is implemented by FlowExecutor class.
+
+A *flow* is a directed, usually but not necessarily acyclic multi-graph of *nodes*
+and *edges* (connections between nodes). The nodes are the computational units and
+the edges define the flow of data between them. The fundamental operations to
+perform on a flow are:
+
+* adding a node
+* removing a node and incident edges
+* adding an edge between a node output and another node's input
+* removing an edge
+
+Flow Execution Modes
+--------------------
+
+There are a couple of different modes / algorithms for executing a flow.
+
+**Data Flow**
+
+In the normal data flow mode, data is simply *forward propagated on change*.
+Specifically, this means the following:
+
+A node output may have 0 or more outgoing connections/edges. When a node's output
+value is updated, the new value is propagated to all connected nodes' inputs. If
+there are multiple edges, the order of activation is undefined.
+
+A node input may have 0 or 1 incoming connections/edges. When a node's input receives
+new data, the node's *update event* is invoked.
+
+A *flow execution* is started once some node's *update event* is invoked (either
+by direct invocation through ``node.update()``, or by receiving input data), or
+some node's output value is updated.
+
+A node can consume inputs and update outputs at any time.
+
+Assumptions:
+
+    * no non-terminating feedback loops.
+
+**Data Flow with Optimization**
+
+Since the naive implementation of the above specification can be highly inefficient
+in some cases, a more advanced algorithm can be used.
+This algorithm ensures that, during a *flow execution*, each *edge* is updated at most
+once.
+It should implement the same semantics as the data flow algorithm, but with a slightly
+tightened assumption:
+
+    * no feedback loops / cycles in the graph
+    * nodes never modify their ports (inputs, outputs) during execution (*update event*)
+
+The additional work required for this at the beginning of a *flow execution* is based
+on a DP algorithm running in :math:`\\mathcal{O}(|V|+ |E|)` time, where
+:math:`|V|` is the number of nodes and
+:math:`|E|` is the number of edges.
+However, when there are multiple consecutive executions without
+any subsequent changes to the graph, this work does not need to be repeated and execution
+is fast.
+
+**Execution Flow**
+
+The special *exec mode* uses an additional type of connection (edge): the
+*execution connection*.
+While pure data flows are the more common use case, some applications call for a slightly
+different paradigm. You can think of the exec mode as e.g. UnrealEngine's blueprint system.
+
+In *exec mode*, calling ``node.exec_output(index)`` has a similar effect as calling
+``node.set_output_val(index, val)`` in *data mode*,
+but without any data being propagated, so it's just a trigger signal.
+Pushing output data, however, does not cause updates in successor nodes.
+
+When a node is updated (it received an *update event* through an *exec connection*), once it
+needs input data (it calls ``self.input(index)``), if that input is connected to some
+predecessor node `P`, then `P` receives an *update event* with ``inp=-1``, during which
+it should push the output data.
+Therefore, data is not forward propagated on change (``node.set_output_val(index, value)``),
+but generated on request (backwards,
+``node.input()`` -> ``pred.update_event()`` -> ``pred.set_output_val()`` -> return).
+
+The *exec mode* is still somewhat experimental, because the *data mode* is the far more
+common use case. It is not yet clear how to best implement the *exec mode* in a way that
+is both efficient and easy to use.
+
+Assumptions:
+
+    * no non-terminating feedback loops with exec connections
+
+"""
+
 from .Base import Base, Event
 from .Data import Data
 from .FlowExecutor import DataFlowNaive, DataFlowOptimized, FlowExecutor, executor_from_flow_alg
 from .Node import Node
-from .NodePort import NodePort
+from .NodePort import NodeOutput, NodeInput
 from .RC import FlowAlg, PortObjPos
-from .utils import node_from_identifier
+from .utils import node_from_identifier, serialize, deserialize
 from typing import List, Dict, Optional, Tuple
 
 
 class Flow(Base):
     """
-    Manages all abstract flow components (nodes, connections) and includes implementations for editing.
+    Manages all abstract flow components (nodes, edges, executors, etc.)
+    and exposes methods for modification.
     """
 
-    def __init__(self, session, script):
+    def __init__(self, session, title):
         Base.__init__(self)
 
         # events
         self.node_added = Event(Node)
         self.node_removed = Event(Node)
-        self.connection_added = Event((NodePort, NodePort))        # Event(Connection)
-        self.connection_removed = Event((NodePort, NodePort))      # Event (Connection)
+        self.connection_added = Event((NodeOutput, NodeInput))        # Event(Connection)
+        self.connection_removed = Event((NodeOutput, NodeInput))      # Event (Connection)
 
         self.connection_request_valid = Event(bool)
         self.nodes_created_from_data = Event(list)
@@ -30,7 +122,7 @@ def __init__(self, session, script):
 
         # general attributes
         self.session = session
-        self.script = script
+        self.title = title
         self.nodes: [Node] = []
 
         self.node_successors = {}   # additional data structure for executors
@@ -40,22 +132,23 @@ def __init__(self, session, script):
         self.alg_mode = FlowAlg.DATA
         self.executor: FlowExecutor = executor_from_flow_alg(self.alg_mode)(self)
 
-    def load(self, data):
-        """Loading a flow from data as previously returned by data()"""
+    def load(self, data: dict):
+        """Loading a flow from data as previously returned by ``Flow.data()``."""
+        super().load(data)
 
         # set algorithm mode
         self.alg_mode = FlowAlg.from_str(data['algorithm mode'])
 
         # build flow
 
-        new_nodes = self.create_nodes_from_data(data['nodes'])
+        new_nodes = self._create_nodes_from_data(data['nodes'])
 
-        self.set_output_values_from_data(new_nodes, data['output data'])
+        self._set_output_values_from_data(new_nodes, data['output data'])
 
-        self.connect_nodes_from_data(new_nodes, data['connections'])
+        self._connect_nodes_from_data(new_nodes, data['connections'])
 
 
-    def create_nodes_from_data(self, nodes_data: List):
+    def _create_nodes_from_data(self, nodes_data: List):
         """create nodes from nodes_data as previously returned by data()"""
 
         nodes = []
@@ -76,13 +169,13 @@ def create_nodes_from_data(self, nodes_data: List):
         return nodes
 
 
-    def set_output_values_from_data(self, nodes: List[Node], data: List):
+    def _set_output_values_from_data(self, nodes: List[Node], data: List):
         for d in data:
             indices = d['dependent node outputs']
             indices_paired = zip(indices[0::2], indices[1::2])
             for node_index, output_index in indices_paired:
                 nodes[node_index].outputs[output_index].val = \
-                    Data(deserialize_from=d['data'])
+                    Data(load_from=deserialize(d['data']))
 
 
     def create_node(self, node_class, data=None):
@@ -91,11 +184,20 @@ def create_node(self, node_class, data=None):
         node = node_class((self, self.session, data))
         node.initialize()
         self.add_node(node)
+
+        # notify addons
+        for addon in self.session.addons.values():
+            addon._on_node_created(self, node)
+
         return node
 
 
     def add_node(self, node: Node):
-        """Stores a node object and causes the node's place_event()"""
+        """
+        Places the node object in the graph, Stores it, and causes the node's
+        ``Node.place_event()`` to be executed. ``Flow.create_node()`` automatically
+        adds the node already, so no need to call this manually.
+        """
 
         self.nodes.append(node)
 
@@ -106,19 +208,20 @@ def add_node(self, node: Node):
             self.graph_adj_rev[inp] = None
 
         node.after_placement()
-        self.flow_changed()
-
-        self.node_added.emit(node)
+        self._flow_changed()
 
+        # notify addons
+        for addon in self.session.addons.values():
+            addon._on_node_added(self, node)
 
-    def node_view_placed(self, node: Node):
-        """Triggered after the node's GUI content has been fully initialized"""
-
-        node.view_place_event()
+        self.node_added.emit(node)
 
 
     def remove_node(self, node: Node):
-        """Removes a node from internal list without deleting it"""
+        """
+        Removes a node from the flow without deleting it. Can be added again
+        with ``Flow.add_node()``.
+        """
 
         node.prepare_removal()
         self.nodes.remove(node)
@@ -129,12 +232,16 @@ def remove_node(self, node: Node):
         for inp in node.inputs:
             del self.graph_adj_rev[inp]
 
-        self.flow_changed()
+        self._flow_changed()
+
+        # notify addons
+        for addon in self.session.addons.values():
+            addon._on_node_removed(self, node)
 
         self.node_removed.emit(node)
 
 
-    def connect_nodes_from_data(self, nodes: List[Node], data: List):
+    def _connect_nodes_from_data(self, nodes: List[Node], data: List):
         connections = []
 
         for c in data:
@@ -160,8 +267,10 @@ def connect_nodes_from_data(self, nodes: List[Node], data: List):
         return connections
 
 
-    def check_connection_validity(self, p1: NodePort, p2: NodePort) -> bool:
-        """Checks whether a considered connect action is legal"""
+    def check_connection_validity(self, p1: NodeOutput, p2: NodeInput) -> bool:
+        """
+        Checks whether a considered connect action is legal.
+        """
 
         valid = True
 
@@ -176,8 +285,12 @@ def check_connection_validity(self, p1: NodePort, p2: NodePort) -> bool:
         return valid
 
 
-    def connect_nodes(self, p1: NodePort, p2: NodePort, silent: bool = False) -> Optional[Tuple[NodePort, NodePort]]:
-        """Connects nodes or disconnects them if they are already connected"""
+    def connect_nodes(self, p1: NodeOutput, p2: NodeInput, silent: bool = False) -> Optional[Tuple[NodeOutput, NodeInput]]:
+        """
+        Connects nodes or disconnects them if they are already connected.
+
+        TODO: change this; rather introduce ``disconnect_nodes()`` instead
+        """
 
         if not self.check_connection_validity(p1, p2):
             return None
@@ -197,8 +310,10 @@ def connect_nodes(self, p1: NodePort, p2: NodePort, silent: bool = False) -> Opt
         return out, inp
 
 
-    def add_connection(self, c: Tuple[NodePort, NodePort], silent: bool = False):
-        """Adds a connection object"""
+    def add_connection(self, c: Tuple[NodeOutput, NodeInput], silent: bool = False):
+        """
+        Adds an edge between two node ports.
+        """
 
         out, inp = c
 
@@ -206,7 +321,7 @@ def add_connection(self, c: Tuple[NodePort, NodePort], silent: bool = False):
         self.graph_adj_rev[inp] = out
 
         self.node_successors[out.node].append(inp.node)
-        self.flow_changed()
+        self._flow_changed()
 
 
         self.executor.conn_added(out, inp, silent=silent)
@@ -214,8 +329,10 @@ def add_connection(self, c: Tuple[NodePort, NodePort], silent: bool = False):
         self.connection_added.emit((out, inp))
 
 
-    def remove_connection(self, c: Tuple[NodePort, NodePort]):
-        """Removes a connection object without deleting it"""
+    def remove_connection(self, c: Tuple[NodeOutput, NodeInput]):
+        """
+        Removes an edge.
+        """
 
         out, inp = c
 
@@ -223,29 +340,41 @@ def remove_connection(self, c: Tuple[NodePort, NodePort]):
         self.graph_adj_rev[inp] = None
 
         self.node_successors[out.node].remove(inp.node)
-        self.flow_changed()
+        self._flow_changed()
 
         self.executor.conn_removed(out, inp)
 #
         self.connection_removed.emit((out, inp))
 
 
-    def connected_inputs(self, out: NodePort) -> List[NodePort]:
+    def connected_inputs(self, out: NodeOutput) -> List[NodeInput]:
+        """
+        Returns a list of all connected inputs to the given output port.
+        """
         return self.graph_adj[out]
 
 
-    def connected_output(self, inp: NodePort) -> Optional[NodePort]:
+    def connected_output(self, inp: NodeInput) -> Optional[NodeOutput]:
+        """
+        Returns the connected output port to the given input port, or
+        ``None`` if it is not connected.
+        """
         return self.graph_adj_rev[inp]
 
 
     def algorithm_mode(self) -> str:
-        """Returns the current algorithm mode of the flow as string"""
+        """
+        Returns the current algorithm mode of the flow as string.
+        """
 
         return FlowAlg.str(self.alg_mode)
 
 
     def set_algorithm_mode(self, mode: str):
-        """Sets the algorithm mode of the flow, possible values are 'data' and 'exec'"""
+        """
+        Sets the algorithm mode of the flow from a string, possible values
+        are 'data', 'data opt', and 'exec'.
+        """
 
         new_alg_mode = FlowAlg.from_str(mode)
         if new_alg_mode is None:
@@ -258,28 +387,32 @@ def set_algorithm_mode(self, mode: str):
         return True
 
 
-    def flow_changed(self):
+    def _flow_changed(self):
         self.executor.flow_changed = True
 
 
     def data(self) -> dict:
-        """Returns a dictionary containing all data of the flow"""
-        return {
+        """
+        Serializes the flow: returns a JSON compatible dict containing all
+        data of the flow.
+        """
+        d = super().data()
+        d.update({
             'algorithm mode': FlowAlg.str(self.alg_mode),
-            'nodes': self.gen_nodes_data(self.nodes),
-            'connections': self.gen_conns_data(self.nodes),
-            'output data': self.gen_output_data(self.nodes),
-            'GID': self.GLOBAL_ID,
-        }
+            'nodes': self._gen_nodes_data(self.nodes),
+            'connections': self._gen_conns_data(self.nodes),
+            'output data': self._gen_output_data(self.nodes),
+        })
+        return d
 
 
-    def gen_nodes_data(self, nodes: List[Node]) -> List[dict]:
+    def _gen_nodes_data(self, nodes: List[Node]) -> List[dict]:
         """Returns the data dicts of the nodes given"""
 
         return [n.data() for n in nodes]
 
 
-    def gen_conns_data(self, nodes: List[Node]) -> List[dict]:
+    def _gen_conns_data(self, nodes: List[Node]) -> List[dict]:
         """Generates the connections data between and relative to the nodes passed"""
 
         # notice that this is intentionally not part of Connection, because connection data
@@ -301,7 +434,7 @@ def gen_conns_data(self, nodes: List[Node]) -> List[dict]:
         return data
 
 
-    def gen_output_data(self, nodes: List[Node]) -> List[Dict]:
+    def _gen_output_data(self, nodes: List[Node]) -> List[Dict]:
         """Serializes output data of the nodes"""
 
         outputs_data = {}
@@ -311,7 +444,7 @@ def gen_output_data(self, nodes: List[Node]) -> List[Dict]:
                 d = out.val
                 if isinstance(d, Data) and d not in outputs_data:
                     outputs_data[d] = {
-                        'data': d.serialize(),
+                        'data': serialize(d.get_data()),
                         'dependent node outputs': [i_n, i_o],
                     }
                 elif isinstance(d, Data):
diff --git a/ryvencore/FlowExecutor.py b/ryvencore/FlowExecutor.py
index 4779df1..b360dba 100644
--- a/ryvencore/FlowExecutor.py
+++ b/ryvencore/FlowExecutor.py
@@ -77,11 +77,11 @@ def input(self, node, index):
             return None
 
     # Node.set_output_val() =>
-    def set_output_val(self, node, index, val):
+    def set_output_val(self, node, index, data):
         out = node.outputs[index]
         if not out.type_ == 'data':
             return
-        out.val = val
+        out.val = data
 
         for inp in self.graph[out]:
             inp.node.update(inp=inp.node.inputs.index(inp))
@@ -153,13 +153,13 @@ def update_node(self, node, inp=-1):
     #   DataFlowNative.input(node, index)
 
     # Node.set_output_val() =>
-    def set_output_val(self, node, index, val):
+    def set_output_val(self, node, index, data):
         out = node.outputs[index]
 
         if self.execution_root_node is None:  # execution starter!
             self.start_execution(root_output=out)
 
-            out.val = val
+            out.val = data
             self.output_updated[out] = True
             self.propagate_output(out)
 
@@ -173,10 +173,10 @@ def set_output_val(self, node, index, val):
                 # there are other possible solutions to this, including running
                 # a new execution analysis of this graph here
 
-                super().set_output_val(node, index, val)
+                super().set_output_val(node, index, data)
 
             else:
-                out.val = val
+                out.val = data
                 self.output_updated[out] = True
 
     # Node.exec_output() =>
@@ -348,9 +348,9 @@ def input(self, node, index):
             return None
 
     # Node.set_output_val() =>
-    def set_output_val(self, node, index, val):
+    def set_output_val(self, node, index, data):
         out = node.outputs[index]
-        out.val = val
+        out.val = data
 
     # Node.exec_output() =>
     def exec_output(self, node, index):
diff --git a/ryvencore/Node.py b/ryvencore/Node.py
index 4836c75..711a94b 100644
--- a/ryvencore/Node.py
+++ b/ryvencore/Node.py
@@ -1,60 +1,54 @@
-import logging
 import traceback
-from typing import List, Dict
+from typing import List, Optional
 
 from .Base import Base
 
 from .NodePort import NodeInput, NodeOutput
-from .NodePortBP import NodeInputBP, NodeOutputBP
+from .NodePortType import NodeInputType, NodeOutputType
 from .RC import FlowAlg
 from .Data import Data
-from .dtypes import DType
 from .InfoMsgs import InfoMsgs
-from .logging import Logger
 from .utils import serialize, deserialize
 
 
 class Node(Base):
     """
     Base class for all node blueprints. Such a blueprint is made by subclassing this class and registering that subclass
-    in the session. Actual node objects are instances of it. The node's static properties are static attributes,
-    which works really well in Python.
-
-    Attributes:
-        static title: the node's title
-        static type_: conditional type field
-        static tags: a list of tag strings, usually for searching
-        static version: version tag, use it!
-        static init_inputs: initial inputs list. the type BP in NodeInputBP stands for 'blueprint', as they only
-            serve as containers, the actual input objects will be created later
-        static init_outputs: initial outputs list, see init_inputs
-        static identifier: unique node identifier string. if not given, the session will set it to the class name
-            on register
-        static identifier_comp: a list of compatible identifiers, useful if you change the class name (and hence
-            the identifier) to provide backward compatibility to older projects
+    in the session. Actual node objects are instances of it. The node's static properties are static attributes.
+    Refer to python's static class attributes behavior.
     """
 
     title = ''
-    type_ = ''
+    """the node's title"""
+
     tags: List[str] = []
-    version: str = None  # None means `undefined` and should be avoided
-    visible: bool = True  # useful field for frontends to indicate invisible nodes which cannot be manually placed
-    
-    init_inputs: List[NodeInputBP] = []
-    init_outputs: List[NodeOutputBP] = []
+    """a list of tag strings, often useful for searching etc."""
 
-    identifier: str = None  # set by Session if None
-    identifier_comp: List[str] = []  # identifier (backwards) compatibility, useful when node class name changes
-    identifier_prefix: str = None  # becomes part of identifier if set, often useful
+    version: str = None
+    """version tag, use it!"""
 
-    """
-    
-    INITIALIZATION
-    
-    """
+    init_inputs: List[NodeInputType] = []
+    """list of node input types determining the initial inputs"""
+
+    init_outputs: List[NodeOutputType] = []
+    """initial outputs list, see ``init_inputs``"""
+
+    identifier: str = None
+    """unique node identifier string. if not given it will set it to the class name when registering in the session"""
+
+    legacy_identifiers: List[str] = []
+    """a list of compatible identifiers, useful if you change the class name (and hence the identifier) to provide 
+    backward compatibility to load old projects that rely on the old identifier"""
+
+    identifier_prefix: str = None
+    """becomes part of the identifier if set; can be useful for grouping nodes"""
+
+    #
+    # INITIALIZATION
+    #
 
     @classmethod
-    def build_identifier(cls):
+    def _build_identifier(cls):
         """
         Sets the identifier to the class name and prepends f"{identifier_prefix}." if
         the identifier prefix is set.
@@ -75,10 +69,8 @@ def __init__(self, params):
         Base.__init__(self)
 
         self.flow, self.session, self.init_data = params
-        self.script = self.flow.script
         self.inputs: List[NodeInput] = []
         self.outputs: List[NodeOutput] = []
-        self.loggers = []
 
         self.initialized = False
 
@@ -87,23 +79,19 @@ def __init__(self, params):
 
     def initialize(self):
         """
-        This method
+        Called by the Flow. This method
 
         - loads all default properties from initial data if it was provided
         - sets up inputs and outputs
-        - enables the logs
         - loads user_data
 
         It does not crash on exception when loading user_data,
         as this is not uncommon when developing nodes.
         """
 
-        # enable logs
-        self.enable_loggers()
-
         if self.init_data:  # load from data
             # setup ports
-            self.setup_ports(self.init_data['inputs'], self.init_data['outputs'])
+            self._setup_ports(self.init_data['inputs'], self.init_data['outputs'])
 
             # set state
             if 'additional data' in self.init_data:
@@ -127,11 +115,11 @@ def initialize(self):
         else:   # default setup
 
             # setup ports
-            self.setup_ports()
+            self._setup_ports()
 
         self.initialized = True
 
-    def setup_ports(self, inputs_data=None, outputs_data=None):
+    def _setup_ports(self, inputs_data=None, outputs_data=None):
 
         if not inputs_data and not outputs_data:
             # generate initial ports
@@ -139,11 +127,7 @@ def setup_ports(self, inputs_data=None, outputs_data=None):
             for i in range(len(self.init_inputs)):
                 inp = self.init_inputs[i]
 
-                if inp.dtype:
-                    self.create_input_dt(dtype=inp.dtype, label=inp.label, add_data=inp.add_data)
-
-                else:
-                    self.create_input(inp.label, inp.type_, add_data=self.init_inputs[i].add_data)
+                self.create_input(inp.label, inp.type_, add_data=self.init_inputs[i].add_data)
 
             for o in range(len(self.init_outputs)):
                 out = self.init_outputs[o]
@@ -154,11 +138,7 @@ def setup_ports(self, inputs_data=None, outputs_data=None):
             # initial ports specifications are irrelevant then
 
             for inp in inputs_data:
-                if 'dtype' in inp:
-                    self.create_input_dt(dtype=DType.from_str(inp['dtype'])(
-                        _load_state=deserialize(inp['dtype state'])), label=inp['label'], add_data=inp)
-                else:
-                    self.create_input(label=inp['label'], type_=inp['type'], add_data=inp)
+                self.create_input(label=inp['label'], type_=inp['type'], add_data=inp)
 
                 # if 'val' in inp:
                 #     # this means the input is 'data' and did not have any connections,
@@ -170,16 +150,14 @@ def setup_ports(self, inputs_data=None, outputs_data=None):
                 self.create_output(out['label'], out['type'])
 
     def after_placement(self):
-        """Called from Flow when the nodes gets added"""
+        """Called from Flow when the nodes gets added."""
 
-        self.enable_loggers()
         self.place_event()
 
     def prepare_removal(self):
-        """Called from Flow when the node gets removed"""
+        """Called from Flow when the node gets removed."""
 
         self.remove_event()
-        self.disable_loggers()
 
     """
     
@@ -193,9 +171,9 @@ def prepare_removal(self):
 
     def update(self, inp=-1):  # , output_called=-1):
         """
-        Activates the node, causing an update_event() if block_updates is not set.
+        Activates the node, causing an ``update_event()`` if ``block_updates`` is not set.
         For performance-, simplicity-, and maintainability-reasons activation is now
-        fully handed over to the operating FlowExecutor, and not managed decentralized
+        fully handed over to the operating ``FlowExecutor``, and not managed decentralized
         in Node, NodePort, and Connection anymore.
         """
 
@@ -211,9 +189,11 @@ def update(self, inp=-1):  # , output_called=-1):
     def update_error(self, e):
         InfoMsgs.write_err('EXCEPTION in', self.title, '\n', traceback.format_exc())
 
-    def input(self, index: int) -> Data:
+    def input(self, index: int) -> Optional[Data]:
         """
-        Returns the value of a data input.
+        Returns the data residing at the data input of given index.
+
+        Do not call on exec inputs.
         """
 
         InfoMsgs.write('input called in', self.title, ':', index)
@@ -221,21 +201,25 @@ def input(self, index: int) -> Data:
         return self.flow.executor.input(self, index)
 
     def exec_output(self, index: int):
-        """Executes an exec output, causing activation of all connections"""
+        """
+        Executes an exec output, causing activation of all connections.
+
+        Do not call on data outputs.
+        """
 
         InfoMsgs.write('executing output', index, 'in:', self.title)
 
         self.flow.executor.exec_output(self, index)
 
-    def set_output_val(self, index, val: Data):
+    def set_output_val(self, index, data: Data):
         """
         Sets the value of a data output causing activation of all connections in data mode.
         """
-        assert isinstance(val, Data), "Output value must be of type ryvencore.Data"
+        assert isinstance(data, Data), "Output value must be of type ryvencore.Data"
 
         InfoMsgs.write('setting output', index, 'in', self.title)
 
-        self.flow.executor.set_output_val(self, index, val)
+        self.flow.executor.set_output_val(self, index, data)
 
     """
     
@@ -247,35 +231,32 @@ def set_output_val(self, index, val: Data):
 
     def update_event(self, inp=-1):
         """
-        Gets called when an input received a signal or some node requested data of an output in exec mode
+        *VIRTUAL*
+
+        Gets called when an input received a signal or some node requested data of an output in exec mode.
+        Implement this in your node class, this is the place where the main processing of your node should happen.
         """
 
         pass
 
     def place_event(self):
         """
-        place_event() is called once the node object has been fully initialized and placed in the flow.
-        When loading content, place_event() is executed *before* the connections are built,
-        which is important for nodes that need to update once and, during this process, set output data values,
-        to prevent later connected (potentially sequential) nodes from receiving false updates because of that.
-        Notice that this method gets executed *every time* the node is added to the flow, which can happen
-        multiple times for the same object, for example due to undo/redo operations.
-        Also note that GUI content is usually not accessible yet from here, for that use view_place_event().
-        """
+        *VIRTUAL*
 
-        pass
+        Called once the node object has been fully initialized and placed in the flow.
+        When loading content, ``place_event()`` is executed *before* connections are built,
+        so updating output values here will not cause any other nodes to be updated during loading.
 
-    def view_place_event(self):
-        """
-        Called once all GUI for the node has been created by the frontend, if one exists.
-        Any initial communication to widgets is supposed to happen here, and this method is not called
-        when running without gui.
+        Notice that this method gets executed *every time* the node is added to the flow, which can happen
+        multiple times for the same object (e.g. due to undo/redo operations).
         """
 
         pass
 
     def remove_event(self):
         """
+        *VIRTUAL*
+
         Called when the node is removed from the flow; useful for stopping threads and timers etc.
         """
 
@@ -283,30 +264,38 @@ def remove_event(self):
 
     def additional_data(self) -> dict:
         """
-        Additional_data()/load_additional_data() is almost equivalent to get_state()/set_state(),
-        but it's often useful for frontends to have their own,
-        get_state()/set_state() then stays clean for all specific node subclasses
+        *VIRTUAL*
+
+        ``additional_data()``/``load_additional_data()`` is almost equivalent to
+        ``get_state()``/``set_state()``,
+        but it turned out to be useful for frontends to have their own dedicated version,
+        so ``get_state()``/``set_state()`` stays clean for all specific node subclasses.
         """
 
         return {}
 
     def load_additional_data(self, data: dict):
         """
-        For loading the data returned by additional_data()
+        *VIRTUAL*
+
+        For loading the data returned by ``additional_data()``.
         """
         pass
 
     def get_state(self) -> dict:
         """
-        Used to store node-specific custom data that needs to be reloaded when loading a project or pasting copied
-        components. All values will be serialized by pickle and base64. The corresponding method for the opposite
-        operation is set_state().
+        *VIRTUAL*
+
+        If your node is stateful, implement this method for serialization. It should return a JSON compatible
+        dict that encodes your node's state. The dict will be passed to ``set_state()`` when the node is loaded.
         """
         return {}
 
     def set_state(self, data: dict, version):
         """
-        Used for reloading node-specific custom data which has been previously returned by get_state()
+        *VIRTUAL*
+
+        Opposite of ``get_state()``, reconstruct any custom internal state here.
         """
         pass
 
@@ -316,33 +305,12 @@ def set_state(self, data: dict, version):
     
     """
 
-    #   LOGGING
-
-    def new_logger(self, title) -> Logger:
-        """Requesting a new custom Log"""
-
-        logger = self.script.logs_manager.new_logger(title)
-        self.loggers.append(logger)
-        return logger
-
-    def disable_loggers(self):
-        """Disables custom logs"""
-
-        for logger in self.loggers:
-            logger.disable()
-            # logger.disabled = True
-
-    def enable_loggers(self):
-        """Enables custom logs"""
-
-        for logger in self.loggers:
-            logger.enable()
-            # logger.enabled = True
-
     #   PORTS
 
     def create_input(self, label: str = '', type_: str = 'data', add_data={}, insert: int = None):
-        """Creates and adds a new input at index pos"""
+        """
+        Creates and adds a new input at the end or index ``insert`` if specified.
+        """
         # InfoMsgs.write('create_input called')
 
         inp = NodeInput(
@@ -357,28 +325,15 @@ def create_input(self, label: str = '', type_: str = 'data', add_data={}, insert
         else:
             self.inputs.append(inp)
 
-    def create_input_dt(self, dtype: DType, label: str = '', add_data={}, insert: int = None):
-        """Creates and adds a new data input with a DType"""
-        # InfoMsgs.write('create_input called')
-
-        inp = NodeInput(
-            node=self,
-            type_='data',
-            label_str=label,
-            dtype=dtype,
-            add_data=add_data,
-        )
-
-        if insert is not None:
-            self.inputs.insert(insert, inp)
-        else:
-            self.inputs.append(inp)
+        return inp
 
     def rename_input(self, index: int, label: str):
         self.inputs[index].label_str = label
 
     def delete_input(self, index: int):
-        """Disconnects and removes input"""
+        """
+        Disconnects and removes an input.
+        """
 
         inp: NodeInput = self.inputs[index]
 
@@ -390,7 +345,9 @@ def delete_input(self, index: int):
         self.inputs.remove(inp)
 
     def create_output(self, label: str = '', type_: str = 'data', insert: int = None):
-        """Creates and adds a new output"""
+        """
+        Creates and adds a new output at the end or index ``insert`` if specified.
+        """
 
         out = NodeOutput(
               node=self,
@@ -403,11 +360,15 @@ def create_output(self, label: str = '', type_: str = 'data', insert: int = None
         else:
             self.outputs.append(out)
 
+        return out
+
     def rename_output(self, index: int, label: str):
         self.outputs[index].label_str = label
 
     def delete_output(self, index: int):
-        """Disconnects and removes output"""
+        """
+        Disconnects and removes output.
+        """
 
         out: NodeOutput = self.outputs[index]
 
@@ -419,31 +380,11 @@ def delete_output(self, index: int):
 
     #   VARIABLES
 
-    def get_vars_manager(self):
-        """Returns a ref to the script's variables manager"""
-
-        return self.script.vars_manager
-
-    def get_var_val(self, name: str):
-        """Gets the value of a script variable"""
-
-        return self.get_vars_manager().get_var_val(name)
-
-    def set_var_val(self, name: str, val):
-        """Sets the value of a script variable"""
-
-        return self.get_vars_manager().set_var(name, val)
-
-    def register_var_receiver(self, name: str, method):
-        """Registers the node with given method as vars receiver in the script's variables manager to catch
-        value changes of any variable with the given name"""
-
-        self.get_vars_manager().register_receiver(self, name, method)
-
-    def unregister_var_receiver(self, name: str, method):
-        """Unregisters previously registered node as receiver for value changes of script variables with given name"""
-
-        self.get_vars_manager().unregister_receiver(self, name, method)
+    def get_addon(self, name: str):
+        """
+        Returns an add-on registered in the session by name, or None if it wasn't found.
+        """
+        return self.session.addons.get(name)
 
     """
     
@@ -460,10 +401,7 @@ def is_active(self):
                 return True
         return False
 
-    def flow_in_data_opt_mode(self):
-        return self.flow.alg_mode == FlowAlg.DATA_OPT
-
-    def inp_connected(self, index):
+    def _inp_connected(self, index):
         return self.flow.connected_output(self.inputs[index]) is not None
 
     """
@@ -474,11 +412,12 @@ def inp_connected(self, index):
 
     def data(self) -> dict:
         """
-        Returns all metadata of the node in JSON-compatible dict.
+        Returns all metadata of the node in JSON-compatible dict, including custom state.
         Used to rebuild the Flow when loading a project or pasting components.
         """
 
-        return {
+        d = super().data()
+        d.update({
             'identifier': self.identifier,
             'version': self.version,
 
@@ -487,6 +426,11 @@ def data(self) -> dict:
 
             'inputs': [i.data() for i in self.inputs],
             'outputs': [o.data() for o in self.outputs],
+        })
+
+        # extend with data from addons
+        for name, addon in self.session.addons.items():
+            # addons can modify anything, there is no isolation enforcement
+            addon._extend_node_data(self, d)
 
-            'GID': self.GLOBAL_ID,
-        }
+        return d
diff --git a/ryvencore/NodePort.py b/ryvencore/NodePort.py
index 18b9833..a50f1fc 100644
--- a/ryvencore/NodePort.py
+++ b/ryvencore/NodePort.py
@@ -1,9 +1,9 @@
+from typing import Optional
+
+from .Data import Data
 from .Base import Base
 
-from .RC import PortObjPos, FlowAlg
-from .dtypes import DType
-from .utils import serialize
-from .InfoMsgs import InfoMsgs
+from .RC import PortObjPos
 
 
 class NodePort(Base):
@@ -18,37 +18,36 @@ def __init__(self, node, io_pos, type_, label_str):
         self.label_str = label_str
 
     def data(self) -> dict:
-        return {
+        d = super().data()
+        d.update({
             'type': self.type_,
             'label': self.label_str,
-            'GID': self.GLOBAL_ID,
-        }
+        })
+        return d
 
 
 class NodeInput(NodePort):
 
-    def __init__(self, node, type_, label_str='', add_data=None, dtype: DType = None):
+    def __init__(self, node, type_, label_str='', add_data=None):
         super().__init__(node, PortObjPos.INPUT, type_, label_str)
 
         # data can be used to store additional data for enhanced data input ports
         self.add_data = add_data
 
-        # optional dtype
-        self.dtype: DType = dtype
-
     def data(self) -> dict:
-        data = super().data()
-
-        if self.dtype:
-            data['dtype'] = str(self.dtype)
-            data['dtype state'] = serialize(self.dtype.get_state())
-
-        return data
-
+        d = super().data()
+        return d
 
 
 class NodeOutput(NodePort):
     def __init__(self, node, type_, label_str=''):
         super().__init__(node, PortObjPos.OUTPUT, type_, label_str)
 
-        self.val = None
+        self.val: Optional[Data] = None
+
+    # def data(self) -> dict:
+    #     data = super().data()
+    #
+    #     data['val'] = self.val if self.val is None else self.val.get_data()
+    #
+    #     return data
diff --git a/ryvencore/NodePortBP.py b/ryvencore/NodePortType.py
similarity index 72%
rename from ryvencore/NodePortBP.py
rename to ryvencore/NodePortType.py
index aeeaca7..dcbddd2 100644
--- a/ryvencore/NodePortBP.py
+++ b/ryvencore/NodePortType.py
@@ -1,7 +1,6 @@
-from .dtypes import DType
 
 # TODO: make this a dataclass
-class NodePortBP:
+class NodePortType:
     """
     The NodePortBP classes are only placeholders (BP = BluePrint) for the static init_input and
     init_outputs of custom Node classes.
@@ -14,14 +13,13 @@ def __init__(self, label: str = '', type_: str = 'data'):
         self.label: str = label
 
 
-class NodeInputBP(NodePortBP):
-    def __init__(self, label: str = '', type_: str = 'data', dtype: DType = None, add_data={}):
+class NodeInputType(NodePortType):
+    def __init__(self, label: str = '', type_: str = 'data', add_data={}):
         super().__init__(label, type_)
 
-        self.dtype = dtype
         self.add_data = add_data
 
 
-class NodeOutputBP(NodePortBP):
+class NodeOutputType(NodePortType):
     def __init__(self, label: str = '', type_: str = 'data'):
         super().__init__(label, type_)
diff --git a/ryvencore/README.md b/ryvencore/README.md
index fd761b9..6456947 100644
--- a/ryvencore/README.md
+++ b/ryvencore/README.md
@@ -18,8 +18,8 @@
 - `Script.py` defines scripts, see comments in code.
 - `Session.py` defines sessions, see comments in code. The session is a projects top-level interface and mainly provides functionality to create, change and delete scripts, and save & load projects.
 
-## packages
+## add-ons
 
-- `dtypes` defines ryvencore's *dtype* system which lets you define dtypes for data inputs of nodes. Conventionally, a frontend implements specific pre-defined widgets for those dtypes which ensure that all values entered through the widget are serializable. Those dtypes might additionally be extended in the future by clearly defined assert conditions, for example to provide serializability guarantees.
+[//]: # (- `dtypes` defines ryvencore's *dtype* system which lets you define dtypes for data inputs of nodes. Conventionally, a frontend implements specific pre-defined widgets for those dtypes which ensure that all values entered through the widget are serializable. Those dtypes might additionally be extended in the future by clearly defined assert conditions, for example to provide serializability guarantees.)
 - `logging` provides some simple logging interfaces to enable a nice and simple logging API for nodes, based on python's built-in `logging` module's basic functionality.
-- `script_variables` defines ryvencore's script vars system which lets you create, change and delete python variables for scripts and register receiver methods for variable names which receive calls when a variable with the according name changes. Nodes have a simple API for this.
+- `variables` defines ryvencore's variables system which lets you create, change and delete python variables for flows and register receiver methods for variable names which receive calls when a variable with the according name changes. Nodes have a simple API for this.
diff --git a/ryvencore/Script.py b/ryvencore/Script.py
deleted file mode 100644
index 54c4d78..0000000
--- a/ryvencore/Script.py
+++ /dev/null
@@ -1,54 +0,0 @@
-from .Base import Base
-from .logging import LogsManager
-from .script_variables import VarsManager
-from .Flow import Flow
-
-
-class Script(Base):
-    """A Script consists of a Flow, a VarsManager and a Logger object."""
-
-    def __init__(self, session, title: str = None, load_data: dict = None, create_default_logs=True):
-        Base.__init__(self)
-
-        self.session = session
-        self.logs_manager = None
-        self._create_default_logs = create_default_logs
-        self.vars_manager = None
-        self.title = title
-        self.flow = None
-
-        if title is None and load_data is not None:
-            self.title = load_data['title'] \
-                if 'title' in load_data else load_data['name']  # backwards compatibility
-
-        self.init_data = load_data
-        self.init_flow_data = None
-        self.init_vars_manager_data = None
-
-        # loading from saved data
-        if load_data:
-            self.init_flow_data = load_data['flow'] if load_data else None
-            self.init_vars_manager_data = load_data['variables']
-
-        # logging
-        self.logs_manager = LogsManager(self, self._create_default_logs)
-
-        # vars manager
-        self.vars_manager = VarsManager(self, self.init_vars_manager_data)
-
-        # flow
-        self.flow = Flow(self.session, self)
-
-
-    def load_flow(self):
-        if self.init_flow_data:
-            self.flow.load(self.init_flow_data)
-
-
-    def data(self) -> dict:
-        return {
-            'title': self.title,
-            'variables': self.vars_manager.data(),
-            'flow': self.flow.data(),
-            'GID': self.GLOBAL_ID,
-        }
diff --git a/ryvencore/Session.py b/ryvencore/Session.py
index f6c0381..e20f0eb 100644
--- a/ryvencore/Session.py
+++ b/ryvencore/Session.py
@@ -1,15 +1,17 @@
-from .Base import Base, Event
-
+import importlib
+import glob
+from typing import List, Dict
 
-from .Script import Script
+from .Base import Base, Event
+from .Flow import Flow
 from .InfoMsgs import InfoMsgs
-
-from typing import List, Dict
+from .utils import pkg_path, load_from_file
+from .Node import Node
 
 
 class Session(Base):
     """
-    The Session is the top level interface to your project. It mainly manages Scripts and registered nodes, and
+    The Session is the top level interface to your project. It mainly manages flows, nodes, and add-ons and
     provides methods for serialization and deserialization of the project.
     """
 
@@ -20,121 +22,185 @@ def __init__(
         Base.__init__(self)
 
         # events
-        self.new_script_created = Event(Script)
-        self.script_renamed = Event(Script)
-        self.script_deleted = Event(Script)
+        self.new_flow_created = Event(Flow)
+        self.flow_renamed = Event(Flow)
+        self.flow_deleted = Event(Flow)
 
         # ATTRIBUTES
-        self.scripts: [Script] = []
+        self.addons = {}
+        self.flows: [Flow] = []
         self.nodes = set()  # list of node CLASSES
         self.invisible_nodes = set()
         self.gui: bool = gui
         self.init_data = None
 
+        self.load_addons(pkg_path('addons/default/'))
+        self.load_addons(pkg_path('addons/'))
+
+
+    def load_addons(self, location: str):
+        """
+        Loads all addons from the given location. ``location`` can be an absolute path to any readable directory.
+        See ``ryvencore.AddOn``.
+        """
+
+        # discover all top-level modules in the given location
+        addons = filter(lambda p: not p.endswith('__init__.py'), glob.glob(location + '/*.py'))
+
+        for path in addons:
+            # extract 'addon' object from module
+            addon, = load_from_file(path, ['addon'])
+
+            if addon is None:
+                continue
+
+            # register addon
+            modname = path.split('/')[-1][:-3]
+            self.addons[modname] = addon
+
+            addon.register(self)
+            setattr(Node, addon.name, addon)
+
 
     def register_nodes(self, node_classes: List):
-        """Registers a list of Nodes which then become available in the flows"""
+        """
+        Registers a list of Nodes which then become available in the flows.
+        Do not attempt to place nodes in flows that haven't been registered in the session before.
+        """
 
         for n in node_classes:
             self.register_node(n)
 
 
     def register_node(self, node_class):
-        """Registers a single Node which then becomes available in the flows"""
+        """
+        Registers a single node.
+        """
 
         # build node class identifier
-        node_class.build_identifier()
+        node_class._build_identifier()
 
         self.nodes.add(node_class)
 
 
     def unregister_node(self, node_class):
-        """Unregisters a Node which will then be removed from the available list.
-        Existing instances won't be affected."""
+        """
+        Unregisters a node which will then be removed from the available list.
+        Existing instances won't be affected.
+        """
 
         self.nodes.remove(node_class)
 
 
     def all_node_objects(self) -> List:
-        """Returns a list of all Node objects instantiated in any flow"""
+        """
+        Returns a list of all node objects instantiated in any flow.
+        """
 
         nodes = []
-        for s in self.scripts:
+        for s in self.flows:
             for n in s.flow.nodes:
                 nodes.append(n)
         return nodes
 
 
-    def create_script(self, title: str = None, create_default_logs=True,
-                      data: Dict = None) -> Script:
-        """Creates and returns a new script.
-        If data is provided the title parameter will be ignored."""
+    def create_flow(self, title: str = None, data: Dict = None) -> Flow:
+        """
+        Creates and returns a new flow.
+        If data is provided the title parameter will be ignored.
+        """
 
-        script = Script(
-            session=self, title=title, create_default_logs=create_default_logs,
-            load_data=data
-        )
+        flow = Flow(session=self, title=title)
+        self.flows.append(flow)
 
-        self.scripts.append(script)
-        script.load_flow()
+        if data:
+            flow.load(data)
 
-        self.new_script_created.emit(script)
+        self.new_flow_created.emit(flow)
 
-        return script
+        return flow
 
 
-    def rename_script(self, script: Script, title: str) -> bool:
-        """Renames an existing script and returns success boolean"""
+    def rename_flow(self, flow: Flow, title: str) -> bool:
+        """
+        Renames an existing flow and returns success boolean.
+        """
 
         success = False
 
-        if self.script_title_valid(title):
-            script.title = title
+        if self.flow_title_valid(title):
+            flow.title = title
             success = True
 
-        self.script_renamed.emit(script)
+        self.flow_renamed.emit(flow)
 
         return success
 
 
-    def script_title_valid(self, title: str) -> bool:
-        """Checks whether a considered title for a new script is valid (unique) or not"""
+    def flow_title_valid(self, title: str) -> bool:
+        """
+        Checks whether a considered title for a new flow is valid (unique) or not.
+        """
 
         if len(title) == 0:
             return False
-        for s in self.scripts:
+        for s in self.flows:
             if s.title == title:
                 return False
 
         return True
 
 
-    def delete_script(self, script: Script):
-        """Removes an existing script."""
+    def delete_flow(self, flow: Flow):
+        """
+        Deletes an existing flow.
+        """
 
-        self.scripts.remove(script)
+        self.flows.remove(flow)
 
-        self.script_deleted.emit(script)
+        self.flow_deleted.emit(flow)
 
 
-    def info_messenger(self):
-        """Returns a reference to InfoMsgs to print info data"""
+    def _info_messenger(self):
+        """
+        Returns a reference to InfoMsgs to print info data.
+        """
 
         return InfoMsgs
 
 
-    def load(self, project: Dict) -> List[Script]:
-        """Loads a project and raises an exception if required nodes are missing"""
+    def load(self, data: Dict) -> List[Flow]:
+        """
+        Loads a project and raises an exception if required nodes are missing (not registered).
+        """
+
+        super().load(data)
+
+        self.init_data = data
+
+        # load flows
+        new_flows = []
 
-        # TODO: perform validity checks
+        #   backward compatibility
+        if 'scripts' in data:
+            flows_data = {
+                title: script_data['flow']
+                for title, script_data in data['scripts'].items()
+            }
+        else:
+            flows_data = data['flows']
 
-        self.init_data = project
+        for fd in flows_data:
+            new_flows.append(self.create_flow(data=fd))
 
-        new_scripts = []
-        for sc in project['scripts']:
-            new_scripts.append(self.create_script(data=sc))
+        # load addons
+        for name, addon_data in data['addons'].items():
+            if name in self.addons:
+                self.addons[name].set_state(addon_data)
+            else:
+                print(f'found missing addon: {name}; attempting to load anyway')
 
-        return new_scripts
+        return new_flows
 
     def serialize(self):
         """Returns the project as JSON compatible dict to be saved and loaded again using load()"""
@@ -143,8 +209,17 @@ def serialize(self):
 
 
     def data(self) -> dict:
-        return {
-            'scripts': [
-                s.data() for s in self.scripts
+        """
+        Serializes the whole project into a JSON compatible dict. Pass to ``load()`` in a new session to restore.
+        """
+
+        d = super().data()
+        d.update({
+            'flows': [
+                s.data() for s in self.flows
             ],
-        }
+            'addons': {
+                name: addon.get_state() for name, addon in self.addons.items()
+            }
+        })
+        return d
diff --git a/ryvencore/__init__.py b/ryvencore/__init__.py
index 41fa667..ff47181 100644
--- a/ryvencore/__init__.py
+++ b/ryvencore/__init__.py
@@ -1,11 +1,9 @@
 from .InfoMsgs import InfoMsgs
 from .RC import *
 from .Session import Session
-from .Script import Script
 from .Flow import Flow
 from .Data import Data
-from .logging import *
+from .AddOn import AddOn
 from .Node import Node
-from .NodePortBP import NodeInputBP, NodeOutputBP
-from .Connection import DataConnection, ExecConnection
+from .NodePortType import NodeInputType, NodeOutputType
 from .utils import serialize, deserialize
diff --git a/ryvencore/addons/__init__.py b/ryvencore/addons/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ryvencore/addons/default/DTypes.py b/ryvencore/addons/default/DTypes.py
new file mode 100644
index 0000000..5591ddb
--- /dev/null
+++ b/ryvencore/addons/default/DTypes.py
@@ -0,0 +1,233 @@
+"""
+This module defines dtypes, definitions of declarative types which can be conveniently used for
+data inputs, and an add-on for those.
+
+These dtypes only store some information which might be used by the nodes, or by a frontend, etc.
+There is currently no type checking or something like that implemented, but might be added
+optionally in the future.
+
+This list may grow significantly over time.
+"""
+
+from typing import Dict as t_Dict, List as t_List
+
+
+class DType:
+    def __init__(self, default, bounds: tuple = None, doc: str = "", _load_state=None):
+
+        self.default = default
+        self.val = self.default
+        self.doc = doc
+        self.bounds = bounds
+
+        if _load_state:
+            self.set_state(_load_state)
+
+        self._data = ['default', 'val', 'doc', 'bounds']
+
+    def __str__(self):
+        return 'DType.'+self.__class__.__name__
+
+    @staticmethod
+    def from_str(s):
+        for DTypeClass in dtypes:
+            if s == 'DType.'+DTypeClass.__name__:
+                return DTypeClass
+
+        return None
+
+    def add_data(self, *attr_names):
+        self._data += list(attr_names)
+
+    def get_state(self) -> dict:
+        return {
+            name: getattr(self, name)
+            for name in self._data
+        }
+
+    def set_state(self, data: dict):
+        for name, val in data.items():
+            setattr(self, name, val)
+
+
+class Data(DType):
+    """Any kind of data represented by some evaluated text input"""
+    def __init__(self, default=None, size: str = 'm', doc: str = "", _load_state=None):
+        """
+        size: 's' / 'm' / 'l'
+        """
+        self.size = size
+        super().__init__(default=default, doc=doc, _load_state=_load_state)
+        self.add_data('size')
+
+
+class Integer(DType):
+    def __init__(self, default: int = 0, bounds: tuple = None, doc: str = "", _load_state=None):
+        super().__init__(default=default, bounds=bounds, doc=doc, _load_state=_load_state)
+
+
+class Float(DType):
+    def __init__(self, default: float = 0.0, bounds: tuple = None, decimals: int = 10, doc: str = "", _load_state=None):
+        self.decimals = decimals
+        super().__init__(default=default, bounds=bounds, doc=doc, _load_state=_load_state)
+        self.add_data('decimals')
+
+
+class Boolean(DType):
+    def __init__(self, default: bool = False, doc: str = "", _load_state=None):
+        super().__init__(default=default, doc=doc, _load_state=_load_state)
+
+
+class Char(DType):
+    def __init__(self, default: chr = '', doc: str = "", _load_state=None):
+        super().__init__(default=default, doc=doc, _load_state=_load_state)
+
+
+class String(DType):
+    def __init__(self, default: str = "", size: str = 'm', doc: str = "", _load_state=None):
+        """
+        size: 's' / 'm' / 'l'
+        """
+        self.size = size
+        super().__init__(default=default, doc=doc, _load_state=_load_state)
+        self.add_data('size')
+
+
+class Choice(DType):
+    def __init__(self, default=None, items: t_List = [], doc: str = "", _load_state=None):
+        self.items = items
+        super().__init__(default=default, doc=doc, _load_state=_load_state)
+        self.add_data('items')
+
+
+class List(DType):
+    def __init__(self, default: t_List = [], doc: str = "", _load_state=None):
+        super().__init__(default=default, doc=doc, _load_state=_load_state)
+
+
+class Date(DType):
+    ...
+
+
+class Time(DType):
+    ...
+
+
+class Color(DType):
+    ...
+
+
+class Range(DType):
+    ...
+
+
+# dtypes = [Data, Integer, Float, Boolean, Char, String, Choice, List]
+
+
+
+'''
+
+Node: ...
+
+    def create_input_dt(self, dtype: DType, label: str = '', add_data={}, insert: int = None):
+        """Creates and adds a new data input with a DType"""
+        # InfoMsgs.write('create_input called')
+
+        inp = NodeInput(
+            node=self,
+            type_='data',
+            label_str=label,
+            dtype=dtype,
+            add_data=add_data,
+        )
+
+        if insert is not None:
+            self.inputs.insert(insert, inp)
+        else:
+            self.inputs.append(inp)
+
+
+NodePort.data(): ...
+
+        if self.dtype:
+            data['dtype'] = str(self.dtype)
+            data['dtype state'] = serialize(self.dtype.get_state())
+'''
+
+from ryvencore import AddOn
+from ryvencore import NodeInputType
+
+
+class DtypesAddon(AddOn):
+    """
+    An add-on that adds the ability to create data inputs with a DType.
+    A DType simply holds some information about the data supposed to be
+    processed at the respective input, but does not perform any typechecks.
+
+    Currently, DType inputs can only be created dynamically through
+    ``DTypes.create_dtype_input(self, ...)``, not in ``init_inputs``. This
+    might change once we found a way of communicating the DType information
+    back to the add-on when node inputs are built from ``init_inputs``,
+    which currently isn't easily possible.
+    """
+
+    name = 'DTypes'
+    version = '0.0.1'
+
+    Data = Data
+    Integer = Integer
+    Float = Float
+    Boolean = Boolean
+    Char = Char
+    String = String
+    Choice = Choice
+    List = List
+
+    class NodeInputType(NodeInputType):
+        def __init__(self, dtype=None, *args, **kwargs):
+            super().__init__(*args, **kwargs)
+
+            self.dtype = dtype
+
+    def __init__(self):
+        super().__init__()
+
+        self.dtype_inputs = {}  # {NodeInput: DType}
+        self.creating_dtype_input = False
+
+    def create_dtype_input(self, node, dtype, *args, **kwargs):
+        """
+        Dynamically creates a new input and assigns the given dtype.
+        """
+
+        self.creating_dtype_input = True
+        inp = node.create_input(*args, **kwargs)
+        self.creating_dtype_input = False
+
+        # dtypes are only on data inputs allowed
+        if inp.type_ != 'data':
+            node.delete_input(node.inputs.index(inp))
+            return
+
+        self.dtype_inputs[inp] = dtype
+
+    def _on_node_created(self, flow, node):
+        """
+        Restores the node's dtypes.
+        """
+
+        for i, inp in enumerate(node.inputs):
+            if inp.type_ == 'data' and 'dtype' in node.init_data['inputs'][i]:
+                dtype = DType.from_str(node.init_data['inputs'][i]['dtype'])
+                dtype.set_state(node.init_data['inputs'][i]['dtype']['state'])
+                self.dtype_inputs[inp] = dtype
+
+    def _extend_node_data(self, node, data: dict):
+        for i, inp in enumerate(node.inputs):
+            if inp in self.dtype_inputs:
+                data['inputs'][i]['dtype'] = {
+                    'type': str(self.dtype_inputs[inp]),
+                    'state': self.dtype_inputs[inp].get_state(),
+                }
+
+# addon = DtypesAddon()
diff --git a/ryvencore/addons/default/Logging.py b/ryvencore/addons/default/Logging.py
new file mode 100644
index 0000000..dd82687
--- /dev/null
+++ b/ryvencore/addons/default/Logging.py
@@ -0,0 +1,107 @@
+"""
+WIP
+"""
+
+from logging import Logger as PyLogger
+from typing import Optional
+
+from ryvencore.AddOn import AddOn
+
+
+class Logger(PyLogger):
+
+    def __init__(self, *args, **kwargs):
+        PyLogger.__init__(self, *args, **kwargs)
+    #     # events
+    #     self.sig_enabled = Event()
+    #     self.sig_disabled = Event()
+
+    def enable(self):
+        # self.sig_enabled.emit()
+        pass
+
+    def disable(self):
+        # self.sig_disabled.emit()
+        pass
+
+
+class LoggingAddon(AddOn):
+    """
+    This addon implements very basic some logging functionality.
+
+    It provides an API to create and delete loggers that are owned
+    by a particular node. The logger gets enabled/disabled
+    automatically when the owning node is added to/removed from
+    the flow.
+
+    Ownership might eventually be expanded to any component that
+    preserves its global ID throughout save and load.
+
+    The contents of logs are currently not preserved. If a log's
+    content should be preserved, it should be saved in a file.
+
+    Refer to Python's logging module documentation.
+    """
+
+    name = 'Logging'
+    version = '0.0.1'
+
+    def __init__(self):
+        super().__init__()
+
+        # logger_created = Event(Logger)
+
+        self.loggers = {}   # {Node: {name: Logger}}
+
+    def new_logger(self, node, title: str) -> Optional[Logger]:
+        """
+        Creates a new logger owned by the node, returns None if
+        one with the given name already exists.
+        """
+
+        if not self._node_is_registered(node):
+            self.loggers[node] = {}
+
+        elif title in self.loggers[node]:
+            return None
+
+        logger = Logger(name=title)
+        self.loggers[node][title] = logger
+        # self.logger_created.emit(logger)
+        return logger
+
+    def _on_node_created(self, flow, node):
+        if node.init_data and 'Logging' in node.init_data:
+            for title in node.init_data['Logging']['loggers']:
+                self.new_logger(node, title)
+                # in case the node already created the logger,
+                # new_logger() will have no effect
+
+    def _node_is_registered(self, node):
+        return node in self.loggers
+
+    def _on_node_added(self, flow, node):
+        if not self._node_is_registered(node):
+            return
+
+        # enable the node's loggers
+        for logger in self.loggers[node].values():
+            logger.enable()
+
+    def _on_node_removed(self, flow, node):
+        if not self._node_is_registered(node):
+            return
+
+        # disable the node's loggers
+        for logger in self.loggers[node].values():
+            logger.disable()
+
+    def _extend_node_data(self, node, data: dict):
+        if not self._node_is_registered(node):
+            return
+
+        data['Logging'] = {
+            'loggers': [name for name in self.loggers[node].keys()]
+        }
+
+addon = LoggingAddon()
diff --git a/ryvencore/addons/default/Variables.py b/ryvencore/addons/default/Variables.py
new file mode 100644
index 0000000..f153b6a
--- /dev/null
+++ b/ryvencore/addons/default/Variables.py
@@ -0,0 +1,227 @@
+from typing import Optional
+
+from ryvencore import Node
+from ryvencore.Base import Base, Event
+from ryvencore.utils import serialize, deserialize
+from ryvencore import AddOn
+
+
+
+class Variable:
+    """Implementation of flow variables"""
+
+    def __init__(self, addon, flow, name='', val=None, data=None):
+        self.addon = addon
+        self.flow = flow
+        self.name = name
+        self.val = val
+
+        if data and 'serialized' in data.keys():
+            self.val = deserialize(data['serialized'])
+
+    def get(self):
+        """
+        Returns the value of the variable
+        """
+        return self.val
+
+    def set(self, val):
+        """
+        Sets the value of the variable
+        """
+        self.val = val
+        self.addon._var_updated(self.flow, self.name)
+
+    def serialize(self):
+        return serialize(self.val)
+
+
+
+
+class VarsAddon(AddOn):
+    """
+    This addon provides a simple variable system.
+
+    It provides an API to create Variable objects which can wrap any Python object.
+
+    Nodes can subscribe to variable names with a callback that is executed once a
+    variable with that name changes or is created. The callback must be a method of
+    the node, so the subscription can be re-established on loading.
+
+    This way nodes can react to changes of data and non-trivial data-flow is introduced,
+    meaning that data dependencies are determined also by variable subscriptions and not
+    purely by the edges in the graph anymore. This can be useful, but it can also prevent
+    optimization. Variables are flow-local.
+
+    >>> import ryvencore as rc
+    >>>
+    >>> class MyNode(rc.Node):
+    ...     init_outputs = []
+    ...
+    ...     def __init__(self, params):
+    ...         super().__init__(params)
+    ...
+    ...         self.Vars = self.get_addon('Variables')
+    ...         self.var_val = None
+    ...
+    ...     def place_event(self):
+    ...         self.Vars.subscribe(self, 'var1', self.var1_changed)
+    ...         self.var_val = self.Vars.var(self.flow, 'var1').get()
+    ...
+    ...     def var1_changed(self, val):
+    ...         print('var1 changed!')
+    ...         self.var_val = val
+    >>>
+    >>> s = rc.Session()
+    >>> s.register_node(MyNode)
+    >>> f = s.create_flow('main')
+    >>>
+    >>> Vars = s.addons['Variables']
+    >>> v = Vars.create_var(f, 'var1', None)
+    >>>
+    >>> n1 = f.create_node(MyNode)
+    >>> v.set(42)
+    var1 changed!
+    >>> print(n1.var_val)
+    42
+    """
+
+    name = 'Variables'
+    version = '0.0.3'
+
+    def __init__(self):
+        AddOn.__init__(self)
+
+        # {
+        #   Flow: {
+        #       'variable name': {
+        #           'var': Variable,
+        #           'subscriptions': [Node method]
+        #        },
+        # }
+        self.flow_variables = {}
+
+    def var_name_valid(self, flow, name: str) -> bool:
+        """
+        Checks if ``name`` is a valid variable identifier and hasn't been take yet.
+        """
+
+        return name.isidentifier() and not self._var_exists(flow, name)
+
+    def create_var(self, flow, name: str, val=None, data=None) -> Optional[Variable]:
+        """
+        Creates and returns a new variable and None if the name isn't valid.
+        """
+
+        if flow not in self.flow_variables:
+            self.flow_variables[flow] = {}
+
+        if self.var_name_valid(flow, name):
+            v = Variable(self, flow, name, val, data)
+            self.flow_variables[flow][name] = {
+                'var': v,
+                'subscriptions': []
+            }
+            return v
+        else:
+            return None
+
+    def delete_var(self, flow, name: str):
+        """
+        Deletes a variable and causes subscription update. Subscriptions are preserved.
+        """
+        if not self._var_exists(flow, name):
+            return
+
+        del self.flow_variables[flow][name]['var']
+
+    def _var_exists(self, flow, name: str) -> bool:
+        return flow in self.flow_variables and name in self.flow_variables[flow]
+
+    def var(self, flow, name: str):
+        """
+        Returns the variable with the given name or None if it doesn't exist.
+        """
+        if not self._var_exists(flow, name):
+            return None
+
+        return self.flow_variables[flow][name]['var']
+
+    def _var_updated(self, flow, name: str):
+        """
+        Called when a Variable object changes or when the var is created or deleted.
+        """
+
+        v = self.flow_variables[flow][name]['var']
+
+        for (node, cb) in self.flow_variables[flow][name]['subscriptions']:
+            cb(v.val)
+
+    def subscribe(self, node: Node, name: str, callback):
+        """
+        Subscribe to a variable. ``callback`` must be a method of the node.
+        """
+        if not self._var_exists(node.flow, name):
+            return
+
+        self.flow_variables[node.flow][name]['subscriptions'].append((node, callback))
+
+    def unsubscribe(self, node, name: str, callback):
+        """
+        Unsubscribe from a variable.
+        """
+        if not self._var_exists(node.flow, name):
+            return
+
+        self.flow_variables[node.flow][name]['subscriptions'].remove((node, callback))
+
+    def _extend_node_data(self, node, data: dict):
+        """
+        Extends the node data with the variable subscriptions.
+        """
+
+        data['Variables'] = {
+            'subscriptions': {
+                name: cb.__name__
+                for name, var in self.flow_variables[node.flow].items()
+                for (n, cb) in var['subscriptions']
+                if node == n
+            }
+        }
+
+        if data['Variables']['subscriptions'] == {}:
+            del data['Variables']
+
+    def _on_node_created(self, flow, node):
+        """
+        Reconstruction of subscriptions.
+        """
+        if node.init_data and 'Variables' in node.init_data:
+            for name, cb_name in node.init_data['Variables']['subscriptions'].items():
+                self.subscribe(node, name, getattr(node, cb_name))
+
+    def get_state(self) -> dict:
+        return {
+            f.GLOBAL_ID: {
+                name: {
+                    'serialized': var['var'].serialize()
+                }
+                for name, var in self.flow_variables[f].items()
+            }
+            for f in self.flow_variables.keys()
+        }
+
+    def set_state(self, state: dict):
+
+        for pref_flow_id, variables in state.items():
+            f = Base.obj_from_prev_id(pref_flow_id)
+
+            # recreate variables
+            for name, var in variables.items():
+                if self._var_exists(f, name):
+                    self.var(f, name).set(deserialize(var['serialized']))
+                else:
+                    self.create_var(f, name, data=var)
+
+
+addon = VarsAddon()
diff --git a/ryvencore/addons/default/__init__.py b/ryvencore/addons/default/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/ryvencore/dtypes/__init__.py b/ryvencore/dtypes/__init__.py
deleted file mode 100644
index e3908a9..0000000
--- a/ryvencore/dtypes/__init__.py
+++ /dev/null
@@ -1,1 +0,0 @@
-from .dtypes import DType, Data, Integer, Float, String, Boolean, Choice, Char, List
diff --git a/ryvencore/dtypes/dtypes.py b/ryvencore/dtypes/dtypes.py
deleted file mode 100644
index 1cb78fc..0000000
--- a/ryvencore/dtypes/dtypes.py
+++ /dev/null
@@ -1,121 +0,0 @@
-"""
-This module contains definitions of declarative types which can be conveniently used for data inputs.
-For each of the exposed types, the frontend can define some prebuilt widgets, which makes nodes
-from other frontends that use those types to determine input widgets automatically compatible in
-other environments.
-This list may grow significantly over time.
-"""
-
-from typing import List as t_List, Dict as t_Dict
-
-
-class DType:
-    def __init__(self, default, bounds: tuple = None, doc: str = "", _load_state=None):
-
-        self.default = default
-        self.val = self.default
-        self.doc = doc
-        self.bounds = bounds
-
-        if _load_state:
-            self.set_state(_load_state)
-
-        self._data = ['default', 'val', 'doc', 'bounds']
-
-    def __str__(self):
-        return 'DType.'+self.__class__.__name__
-
-    @staticmethod
-    def from_str(s):
-        for DTypeClass in dtypes:
-            if s == 'DType.'+DTypeClass.__name__:
-                return DTypeClass
-
-        return None
-
-    def add_data(self, *attr_names):
-        self._data += list(attr_names)
-
-    def get_state(self) -> dict:
-        return {
-            name: getattr(self, name)
-            for name in self._data
-        }
-
-    def set_state(self, data: dict):
-        for name, val in data.items():
-            setattr(self, name, val)
-
-
-class Data(DType):
-    """Any kind of data represented by some evaluated text input"""
-    def __init__(self, default=None, size: str = 'm', doc: str = "", _load_state=None):
-        """
-        size: 's' / 'm' / 'l'
-        """
-        self.size = size
-        super().__init__(default=default, doc=doc, _load_state=_load_state)
-        self.add_data('size')
-
-
-class Integer(DType):
-    def __init__(self, default: int = 0, bounds: tuple = None, doc: str = "", _load_state=None):
-        super().__init__(default=default, bounds=bounds, doc=doc, _load_state=_load_state)
-
-
-class Float(DType):
-    def __init__(self, default: float = 0.0, bounds: tuple = None, decimals: int = 10, doc: str = "", _load_state=None):
-        self.decimals = decimals
-        super().__init__(default=default, bounds=bounds, doc=doc, _load_state=_load_state)
-        self.add_data('decimals')
-
-
-class Boolean(DType):
-    def __init__(self, default: bool = False, doc: str = "", _load_state=None):
-        super().__init__(default=default, doc=doc, _load_state=_load_state)
-
-
-class Char(DType):
-    def __init__(self, default: chr = '', doc: str = "", _load_state=None):
-        super().__init__(default=default, doc=doc, _load_state=_load_state)
-
-
-class String(DType):
-    def __init__(self, default: str = "", size: str = 'm', doc: str = "", _load_state=None):
-        """
-        size: 's' / 'm' / 'l'
-        """
-        self.size = size
-        super().__init__(default=default, doc=doc, _load_state=_load_state)
-        self.add_data('size')
-
-
-class Choice(DType):
-    def __init__(self, default=None, items: t_List = [], doc: str = "", _load_state=None):
-        self.items = items
-        super().__init__(default=default, doc=doc, _load_state=_load_state)
-        self.add_data('items')
-
-
-class List(DType):
-    def __init__(self, default: t_List = [], doc: str = "", _load_state=None):
-        super().__init__(default=default, doc=doc, _load_state=_load_state)
-
-
-class Date(DType):
-    ...
-
-
-class Time(DType):
-    ...
-
-
-class Color(DType):
-    ...
-
-
-class Range(DType):
-    ...
-
-
-dtypes = [Data, Integer, Float, Boolean, Char, String, Choice, List]
diff --git a/ryvencore/logging/Logger.py b/ryvencore/logging/Logger.py
deleted file mode 100644
index 963457c..0000000
--- a/ryvencore/logging/Logger.py
+++ /dev/null
@@ -1,23 +0,0 @@
-from logging import Logger as PyLogger
-from ..Base import Base, Event
-
-
-class Logger(Base, PyLogger):
-    """
-    A small wrapper template for the python loggers to add functionality on node events.
-    Reimplemented as wrapper by the frontend with according implementations of the below methods.
-    """
-
-    def __init__(self, *args, **kwargs):
-        Base.__init__(self)
-        PyLogger.__init__(self, *args, **kwargs)
-
-        # events
-        self.sig_enabled = Event()
-        self.sig_disabled = Event()
-
-    def enable(self):
-        self.sig_enabled.emit()
-
-    def disable(self):
-        self.sig_disabled.emit()
diff --git a/ryvencore/logging/LogsManager.py b/ryvencore/logging/LogsManager.py
deleted file mode 100644
index da87878..0000000
--- a/ryvencore/logging/LogsManager.py
+++ /dev/null
@@ -1,34 +0,0 @@
-from ..Base import Base, Event
-from .Logger import Logger
-
-
-class LogsManager(Base):
-    """Manages all logs/loggers that belong to the script."""
-
-    def __init__(self, script, create_default_logs=True):
-        Base.__init__(self)
-
-        # events
-        self.new_logger_created = Event(Logger)
-
-        self.script = script
-        self.session = self.script.session
-        self.loggers: [Logger] = []
-        self.default_loggers = {
-            'global': None,
-            'errors': None,
-        }
-
-        if create_default_logs:
-            self.create_default_loggers()
-
-    def create_default_loggers(self):
-        for name in self.default_loggers.keys():
-            self.default_loggers[name] = self.new_logger(title=name.title())
-
-    def new_logger(self, title: str) -> Logger:
-        # logger = self.session.CLASSES['logger'](name=title)
-        logger = Logger(name=title)
-        self.loggers.append(logger)
-        self.new_logger_created.emit(logger)
-        return logger
diff --git a/ryvencore/logging/__init__.py b/ryvencore/logging/__init__.py
deleted file mode 100644
index 07ede40..0000000
--- a/ryvencore/logging/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .LogsManager import LogsManager
-from .Logger import Logger
diff --git a/ryvencore/script_variables/Variable.py b/ryvencore/script_variables/Variable.py
deleted file mode 100644
index 701c053..0000000
--- a/ryvencore/script_variables/Variable.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from ..utils import serialize, deserialize
-
-
-class Variable:
-    """Implementation of script variables"""
-
-    def __init__(self, name='', val=None):
-        self.name = name
-        self.val = None
-
-        if val and 'serialized' in val.keys():
-            self.val = deserialize(val['serialized'])
-
-    def serialize(self):
-        return serialize(self.val)
diff --git a/ryvencore/script_variables/VarsManager.py b/ryvencore/script_variables/VarsManager.py
deleted file mode 100644
index 54e5bb9..0000000
--- a/ryvencore/script_variables/VarsManager.py
+++ /dev/null
@@ -1,133 +0,0 @@
-from ..Base import Base, Event
-from .Variable import Variable
-from ..InfoMsgs import InfoMsgs
-
-
-class VarsManager(Base):
-    """Manages script variables and triggers receivers when values of variables change"""
-
-    def __init__(self, script, load_data=None):
-        Base.__init__(self)
-
-        # events
-        self.new_var_created = Event(Variable)
-        self.var_deleted = Event(Variable)
-        self.var_val_changed = Event(Variable, object)
-
-
-        self.script = script
-        self.variables: [Variable] = []
-        self.var_receivers = {}
-
-        if load_data:
-            for name in load_data.keys():  # variables
-                self.create_new_var(name, val=load_data[name])
-
-
-    def var_name_valid(self, name: str) -> bool:
-        """Checks if a var name candidate is valid"""
-
-        if len(name) == 0:
-            return False
-
-        # search for name issues
-        for v in self.variables:
-            if v.name == name:
-                return False
-
-        return True
-
-
-    def create_new_var(self, name: str, val=None) -> Variable:
-        """Creates and returns a new script variable and None if the name isn't valid"""
-
-        if self.var_name_valid(name):
-            v = Variable(name, val)
-            self.variables.append(v)
-            self.var_receivers[name] = {}
-            self.new_var_created.emit(v)
-            return v
-        else:
-            return None
-
-
-    def get_var(self, name) -> Variable:
-        """Returns script variable with given name or None if it couldn't be found"""
-
-        InfoMsgs.write('getting variable with name:', name)
-
-        for v in self.variables:
-            if v.name == name:
-                return v
-        return None
-
-
-    def get_var_val(self, name):
-        """Returns the value of a script variable with given name or None if it couldn't be found"""
-
-        var = self.get_var(name)
-        return var.val if var is not None else None
-
-
-    def set_var(self, name, val) -> bool:
-        """Sets the value of an existing script variable.
-        Returns true in case of success, false if the var couldn't be found and set."""
-
-        var_index = self._get_var_index_from_name(name)
-        if var_index is None:
-            return False
-
-        var = self.variables[var_index]
-        var.val = val
-
-        self.var_val_changed.emit(val)
-
-        # and update all variable usages by calling all registered object's methods on updated variable with the new val
-        for receiver, methods in self.var_receivers[name].items():
-            for m in methods:
-                m(name, val)
-
-        return True
-
-
-    def _get_var_index_from_name(self, name):
-        var_names_list = [v.name for v in self.variables]
-        for i in range(len(var_names_list)):
-            if var_names_list[i] == name:
-                return i
-
-        return None
-
-
-    def delete_var(self, var: Variable):
-        """Deletes a variable"""
-
-        self.variables.remove(var)
-        self.var_deleted.emit(var)
-
-
-    def register_receiver(self, receiver, var_name: str, method):
-        """A registered receiver (method) gets triggered every time the
-        value of a variable with the given name changes (also when it gets created)."""
-
-        if receiver in self.var_receivers[var_name]:
-            self.var_receivers[var_name][receiver].append(method)
-        else:
-            self.var_receivers[var_name][receiver] = [method]
-
-
-    def unregister_receiver(self, receiver, var_name: str, method) -> bool:
-        """Unregisters a method and returns true in case of success. See also register_receiver()."""
-
-        if receiver in self.var_receivers[var_name]:
-            self.var_receivers[var_name][receiver].remove(method)
-            return True
-        else:
-            return False
-
-
-    def data(self) -> dict:
-        vars_dict = {}
-        for v in self.variables:
-            vars_dict[v.name] = {'serialized': v.serialize()}
-        return vars_dict
diff --git a/ryvencore/script_variables/__init__.py b/ryvencore/script_variables/__init__.py
deleted file mode 100644
index 5f1ad57..0000000
--- a/ryvencore/script_variables/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .VarsManager import VarsManager
-from .Variable import Variable
diff --git a/ryvencore/utils.py b/ryvencore/utils.py
index b9fb436..2532810 100644
--- a/ryvencore/utils.py
+++ b/ryvencore/utils.py
@@ -1,8 +1,10 @@
 """A collection of useful functions used by different components."""
 
 import base64
+import importlib.util
 import pickle
-from typing import List
+from os.path import dirname, abspath, join, basename
+from typing import List, Tuple
 
 
 def serialize(data) -> str:
@@ -20,7 +22,7 @@ def node_from_identifier(identifier: str, nodes: List):
             return nc
     else:  # couldn't find a node with this identifier => search for identifier_comp
         for nc in nodes:
-            if identifier in nc.identifier_comp:
+            if identifier in nc.legacy_identifiers:
                 return nc
         else:
             raise Exception(
@@ -29,3 +31,37 @@ def node_from_identifier(identifier: str, nodes: List):
                 f'identifier to the identifier_comp list attribute to provide '
                 f'backwards compatibility.'
             )
+
+def pkg_path(subpath: str = None):
+    """
+    Returns the path to the installed package root directory, optionally with a relative sub-path appended.
+    Notice that this returns the path to the ryvencore package (ryvencore/ryvencore/) not the repository (ryvencore/).
+    """
+
+    p = dirname(__file__)
+    if subpath is not None:
+        p = join(p, subpath)
+    return abspath(p)
+
+
+def load_from_file(file: str, comps: List[str]) -> Tuple:
+    """
+    Imports components with name in ``comps`` from a python module.
+    """
+    # https://stackoverflow.com/questions/67631/how-do-i-import-a-module-given-the-full-path
+
+    name = basename(file).split('.')[0]
+    spec = importlib.util.spec_from_file_location(name, file)
+    importlib.util.module_from_spec(spec)
+    mod = spec.loader.load_module(name)
+    # using load_module(name) instead of exec_module(mod) here,
+    # because exec_module() somehow then registers it as "built-in"
+    # which is wrong and e.g. prevents inspect from parsing the source
+
+    def get_comp(c):
+        try:
+            return getattr(mod, c)
+        except AttributeError:
+            return None
+
+    return tuple([get_comp(c) for c in comps])
diff --git a/setup.cfg b/setup.cfg
index 5d6513f..b22ceca 100644
--- a/setup.cfg
+++ b/setup.cfg
@@ -1,6 +1,6 @@
 [metadata]
 name = ryvencore
-version = v0.3.1.1
+version = v0.4.0a9
 author = Leon Thomm
 author_email = l.thomm@mailbox.org
 description = Python backend for node editor-like graph-based processing


Test patch:
diff --git a/test.py b/test.py
index 828d9ce..a8d2504 100644
--- a/test.py
+++ b/test.py
@@ -1,45 +1,127 @@
+import json
 import unittest
 import ryvencore as rc
+from ryvencore import Data
+from ryvencore.addons.default.Variables import addon as Variables
+from ryvencore.addons.default.Logging import addon as Logging
 
 
-class DataFlowBasic(unittest.TestCase):
+class NodeBase(rc.Node):
 
-    class Node1(rc.Node):
-        title = 'node 1'
-        init_inputs = []
-        init_outputs = [rc.NodeOutputBP(type_='data'), rc.NodeOutputBP(type_='data')]
+    def __init__(self, params):
+        super().__init__(params)
 
-        def update_event(self, inp=-1):
-            self.set_output_val(0, 'Hello, World!')
-            self.set_output_val(1, 42)
-            print('finished')
+        self.Vars: Variables = self.get_addon('Variables')
+        self.Logging: Logging = self.get_addon('Logging')
 
-    class Node2(rc.Node):
-        title = 'node 2'
-        init_inputs = [rc.NodeInputBP('data')]
-        init_outputs = []
+    def place_event(self):
+        if not self.Vars._var_exists(self.flow, 'var1'):
+            self.Vars.create_var(self.flow, 'var1', 'Hello, World!')
 
-        def update_event(self, inp=-1):
-            print(f'received data on input {inp}: {self.input(inp)}')
+
+class Node1(NodeBase):
+    title = 'node 1'
+    init_inputs = []
+    init_outputs = [rc.NodeOutputType(type_='data'), rc.NodeOutputType(type_='data')]
+
+    def __init__(self, params):
+        super().__init__(params)
+
+        self.var_val = None
+
+        self.log1 = self.Logging.new_logger(self, 'log1')
+        self.log2 = self.Logging.new_logger(self, 'log2')
+
+    def subscribe_to_var1(self):
+        self.Vars.subscribe(self, 'var1', self.var1_changed)
+        self.var_val = self.Vars.var(self.flow, 'var1').get()
+
+    def update_event(self, inp=-1):
+        self.set_output_val(0, Data('Hello, World!'))
+        self.set_output_val(1, Data(42))
+        print('finished')
+
+    def var1_changed(self, val):
+        self.var_val = val
+        print('var1 changed in slot:', val)
+        self.update()
+
+
+class Node2(NodeBase):
+    title = 'node 2'
+    init_inputs = [rc.NodeInputType()]
+    init_outputs = []
+
+    def update_event(self, inp=-1):
+        print(f'received data on input {inp}: {self.input(inp)}')
+
+    def update_var1(self, val):
+        self.Vars.var(self.flow, 'var1').set(val)
+        print('var1 successfully updated:', val)
+
+
+class DataFlowBasic(unittest.TestCase):
 
     def runTest(self):
         s = rc.Session()
-        f = s.create_script('main').flow
+        s.register_nodes([Node1, Node2])
 
-        n1 = f.create_node(self.Node1)
-        n2 = f.create_node(self.Node2)
-        n3 = f.create_node(self.Node2)
+        f = s.create_flow('main')
+
+        n1 = f.create_node(Node1)
+        n2 = f.create_node(Node2)
+        n3 = f.create_node(Node2)
+        n4 = f.create_node(Node2)
 
         f.connect_nodes(n1.outputs[0], n2.inputs[0])
         f.connect_nodes(n1.outputs[1], n3.inputs[0])
+        f.connect_nodes(n1.outputs[1], n4.inputs[0])
+
+        # test data model
 
         self.assertEqual(n1.outputs[0].val, None)
         self.assertEqual(n1.outputs[1].val, None)
 
         n1.update()
 
-        self.assertEqual(n1.outputs[0].val, 'Hello, World!')
-        self.assertEqual(n1.outputs[1].val, 42)
+        self.assertEqual(n1.outputs[0].val.payload, 'Hello, World!')
+        self.assertEqual(n1.outputs[1].val.payload, 42)
+        self.assertEqual(n3.input(0), n4.input(0))
+
+        # test variables addon
+
+        n1.subscribe_to_var1()
+        n2.update_var1(42)
+        assert n1.var_val == 42
+
+        print('----------------------------------------------------------')
+
+        # test save and load
+
+        project = s.serialize()
+        print(json.dumps(project, indent=4))
+        del s
+
+        s2 = rc.Session()
+        s2.register_nodes([Node1, Node2])
+        s2.load(project)
+
+        vars = s2.addons.get('Variables')
+
+        f2 = s2.flows[0]
+        assert vars.var(f2, 'var1').get() == 42
+
+        n1_2, n2_2, n3_2, n4_2 = f2.nodes
+
+        assert n1_2.var_val == 42
+        assert n2_2.input(0).payload == 'Hello, World!'
+        assert n3_2.input(0).payload == 42
+        assert n4_2.input(0).payload == 42
+
+        n1_2.update()
+        n2_2.update_var1(43)
+
+        assert n1_2.var_val == 43
 
 
 class ExecFlowBasic(unittest.TestCase):
@@ -47,16 +129,16 @@ class ExecFlowBasic(unittest.TestCase):
     class Node1(rc.Node):
         title = 'node 1'
         init_inputs = []
-        init_outputs = [rc.NodeOutputBP(type_='exec'), rc.NodeOutputBP(type_='data')]
+        init_outputs = [rc.NodeOutputType(type_='exec'), rc.NodeOutputType(type_='data')]
 
         def update_event(self, inp=-1):
-            self.set_output_val(1, 'Hello, World!')
+            self.set_output_val(1, Data('Hello, World!'))
             self.exec_output(0)
             print('finished')
 
     class Node2(rc.Node):
         title = 'node 2'
-        init_inputs = [rc.NodeInputBP(type_='exec'), rc.NodeInputBP(type_='data')]
+        init_inputs = [rc.NodeInputType(type_='exec'), rc.NodeInputType(type_='data')]
         init_outputs = []
 
         def __init__(self, params):
@@ -70,13 +152,14 @@ def update_event(self, inp=-1):
 
 
     def runTest(self):
+        # rc.InfoMsgs.enable(True)
         s = rc.Session()
-        f = s.create_script('main').flow
+        f = s.create_flow('main').flow
         f.set_algorithm_mode('exec')
 
-        n1 = f.create_node(self.Node1)
-        n2 = f.create_node(self.Node2)
-        n3 = f.create_node(self.Node2)
+        n1 = f.create_node(Node1)
+        n2 = f.create_node(Node2)
+        n3 = f.create_node(Node2)
 
         f.connect_nodes(n1.outputs[0], n2.inputs[0])
         f.connect_nodes(n1.outputs[1], n2.inputs[1])
@@ -88,6 +171,6 @@ def runTest(self):
 
         n1.update()
 
-        self.assertEqual(n1.outputs[1].val, 'Hello, World!')
-        self.assertEqual(n2.data, 'Hello, World!')
-        self.assertEqual(n3.data, None)
+        self.assertEqual(n1.outputs[1].val.payload, 'Hello, World!')
+        self.assertEqual(n2.val.payload, 'Hello, World!')
+        self.assertEqual(n3.val, None)