Repository: pedohorse/lifeblood (language: python)

Issue:
arbitrary resource definitions
1) allow arbitrary per-machine resource definitions
2) introduce the concept of devices
3) introduce the concept of global resources


Discussion / hints:

Fix patch:
diff --git a/src/lifeblood/basenode.py b/src/lifeblood/basenode.py
index 0fede7f8..d717b581 100644
--- a/src/lifeblood/basenode.py
+++ b/src/lifeblood/basenode.py
@@ -1,12 +1,10 @@
 import asyncio
-import re
 from copy import deepcopy
 from typing import Dict, Optional, Any
 from .nodethings import ProcessingResult
 from .uidata import NodeUi, ParameterNotFound, Parameter
 from .processingcontext import ProcessingContext
 from .logging import get_logger
-from .enums import NodeParameterType, WorkerType
 from .plugin_info import PluginInfo, empty_plugin_info
 from .nodegraph_holder_base import NodeGraphHolderBase
 
@@ -56,6 +54,9 @@ def set_parent(self, graph_holder: NodeGraphHolderBase, node_id_in_graph: int):
         self.__parent = graph_holder
         self.__parent_nid = node_id_in_graph
 
+    def parent(self) -> Optional[NodeGraphHolderBase]:
+        return self.__parent
+
     def logger(self) -> "Logger":
         return self.__logger
 
@@ -167,11 +168,11 @@ def _ui_changed(self, definition_changed=False):
     #         #  this may also apply to _ui_changed above, but nodes really SHOULD NOT change their own parameters during processing
     #         asyncio.get_event_loop().create_task(self.__parent.node_reports_changes_needs_saving(self.__parent_nid))
 
-    def _process_task_wrapper(self, task_dict) -> ProcessingResult:
+    def _process_task_wrapper(self, task_dict, node_config) -> ProcessingResult:
         # with self.get_ui().lock_interface_readonly():  # TODO: this is bad, RETHINK!
         #  TODO: , in case threads do l1---r1    - release2 WILL leave lock in locked state forever, as it remembered it at l2
         #  TODO:                         l2---r2
-        return self.process_task(ProcessingContext(self, task_dict))
+        return self.process_task(ProcessingContext(self, task_dict, node_config))
 
     def process_task(self, context: ProcessingContext) -> ProcessingResult:
         """
@@ -182,9 +183,9 @@ def process_task(self, context: ProcessingContext) -> ProcessingResult:
         """
         raise NotImplementedError()
 
-    def _postprocess_task_wrapper(self, task_dict) -> ProcessingResult:
+    def _postprocess_task_wrapper(self, task_dict, node_config) -> ProcessingResult:
         # with self.get_ui().lock_interface_readonly():  #TODO: read comment for _process_task_wrapper
-        return self.postprocess_task(ProcessingContext(self, task_dict))
+        return self.postprocess_task(ProcessingContext(self, task_dict, node_config))
 
     def postprocess_task(self, context: ProcessingContext) -> ProcessingResult:
         """
@@ -252,96 +253,3 @@ def set_state(self, state: dict):
         restore state as given by get_state
         """
         pass
-
-
-class BaseNodeWithTaskRequirements(BaseNode):
-    def __init__(self, name: str):
-        super(BaseNodeWithTaskRequirements, self).__init__(name)
-        ui = self.get_ui()
-        with ui.initializing_interface_lock():
-            with ui.collapsable_group_block('main worker requirements', 'worker requirements'):
-                ui.add_parameter('priority adjustment', 'priority adjustment', NodeParameterType.FLOAT, 0).set_slider_visualization(-100, 100)
-                with ui.parameters_on_same_line_block():
-                    ui.add_parameter('worker cpu cost', 'min <cpu (cores)> preferred', NodeParameterType.FLOAT, 1.0).set_value_limits(value_min=0)
-                    ui.add_parameter('worker cpu cost preferred', None, NodeParameterType.FLOAT, 0.0).set_value_limits(value_min=0)
-                with ui.parameters_on_same_line_block():
-                    ui.add_parameter('worker mem cost', 'min <memory (GBs)> preferred', NodeParameterType.FLOAT, 0.5).set_value_limits(value_min=0)
-                    ui.add_parameter('worker mem cost preferred', None, NodeParameterType.FLOAT, 0.0).set_value_limits(value_min=0)
-                ui.add_parameter('worker groups', 'groups (space or comma separated)', NodeParameterType.STRING, '')
-                ui.add_parameter('worker type', 'worker type', NodeParameterType.INT, WorkerType.STANDARD.value)\
-                    .add_menu((('standard', WorkerType.STANDARD.value),
-                               ('scheduler helper', WorkerType.SCHEDULER_HELPER.value)))
-                with ui.collapsable_group_block('gpu main worker requirements', 'gpu requirements'):
-                    with ui.parameters_on_same_line_block():
-                        ui.add_parameter('worker gpu cost', 'min <gpus> preferred', NodeParameterType.FLOAT, 0.0).set_value_limits(value_min=0)
-                        ui.add_parameter('worker gpu cost preferred', None, NodeParameterType.FLOAT, 0.0).set_value_limits(value_min=0)
-                    with ui.parameters_on_same_line_block():
-                        ui.add_parameter('worker gpu mem cost', 'min <memory (GBs)> preferred', NodeParameterType.FLOAT, 0.0).set_value_limits(value_min=0)
-                        ui.add_parameter('worker gpu mem cost preferred', None, NodeParameterType.FLOAT, 0.0).set_value_limits(value_min=0)
-
-    def __apply_requirements(self, task_dict: dict, result: ProcessingResult):
-        if result.invocation_job is not None:
-            context = ProcessingContext(self, task_dict)
-            raw_groups = context.param_value('worker groups').strip()
-            reqs = result.invocation_job.requirements()
-            if raw_groups != '':
-                reqs.add_groups(re.split(r'[ ,]+', raw_groups))
-            reqs.set_min_cpu_count(context.param_value('worker cpu cost'))
-            reqs.set_min_memory_bytes(context.param_value('worker mem cost') * 10**9)
-            reqs.set_min_gpu_count(context.param_value('worker gpu cost'))
-            reqs.set_min_gpu_memory_bytes(context.param_value('worker gpu mem cost') * 10**9)
-            # preferred
-            pref_cpu_count = context.param_value('worker cpu cost preferred')
-            pref_mem_bytes = context.param_value('worker mem cost preferred') * 10**9
-            pref_gpu_count = context.param_value('worker gpu cost preferred')
-            pref_gpu_mem_bytes = context.param_value('worker gpu mem cost preferred') * 10**9
-            if pref_cpu_count > 0:
-                reqs.set_preferred_cpu_count(pref_cpu_count)
-            if pref_mem_bytes > 0:
-                reqs.set_preferred_memory_bytes(pref_mem_bytes)
-            if pref_gpu_count > 0:
-                reqs.set_preferred_gpu_count(pref_gpu_count)
-            if pref_gpu_mem_bytes > 0:
-                reqs.set_preferred_gpu_memory_bytes(pref_gpu_mem_bytes)
-
-            reqs.set_worker_type(WorkerType(context.param_value('worker type')))
-            result.invocation_job.set_requirements(reqs)
-            result.invocation_job.set_priority(context.param_value('priority adjustment'))
-        return result
-
-    def _process_task_wrapper(self, task_dict) -> ProcessingResult:
-        result = super(BaseNodeWithTaskRequirements, self)._process_task_wrapper(task_dict)
-        return self.__apply_requirements(task_dict, result)
-
-    def _postprocess_task_wrapper(self, task_dict) -> ProcessingResult:
-        result = super(BaseNodeWithTaskRequirements, self)._postprocess_task_wrapper(task_dict)
-        return self.__apply_requirements(task_dict, result)
-
-
-# class BaseNodeWithEnvironmentRequirements(BaseNode):
-#     def __init__(self, name: str):
-#         super(BaseNodeWithEnvironmentRequirements, self).__init__(name)
-#         ui = self.get_ui()
-#         with ui.initializing_interface_lock():
-#             with ui.collapsable_group_block('main environment resolver', 'task environment resolver additional requirements'):
-#                 ui.add_parameter('main env resolver name', 'resolver name', NodeParameterType.STRING, 'StandardEnvironmentResolver')
-#                 with ui.multigroup_parameter_block('main env resolver arguments'):
-#                     with ui.parameters_on_same_line_block():
-#                         type_param = ui.add_parameter('main env resolver arg type', '', NodeParameterType.INT, 0)
-#                         type_param.add_menu((('int', NodeParameterType.INT.value),
-#                                              ('bool', NodeParameterType.BOOL.value),
-#                                              ('float', NodeParameterType.FLOAT.value),
-#                                              ('string', NodeParameterType.STRING.value),
-#                                              ('json', -1)
-#                                              ))
-#
-#                         ui.add_parameter('main env resolver arg svalue', 'val', NodeParameterType.STRING, '').append_visibility_condition(type_param, '==', NodeParameterType.STRING.value)
-#                         ui.add_parameter('main env resolver arg ivalue', 'val', NodeParameterType.INT, 0).append_visibility_condition(type_param, '==', NodeParameterType.INT.value)
-#                         ui.add_parameter('main env resolver arg fvalue', 'val', NodeParameterType.FLOAT, 0.0).append_visibility_condition(type_param, '==', NodeParameterType.FLOAT.value)
-#                         ui.add_parameter('main env resolver arg bvalue', 'val', NodeParameterType.BOOL, False).append_visibility_condition(type_param, '==', NodeParameterType.BOOL.value)
-#                         ui.add_parameter('main env resolver arg jvalue', 'val', NodeParameterType.STRING, '').append_visibility_condition(type_param, '==', -1)
-#
-#     def _process_task_wrapper(self, task_dict) -> ProcessingResult:
-#         result = super(BaseNodeWithEnvironmentRequirements, self)._process_task_wrapper(task_dict)
-#         result.invocation_job.environment_resolver_arguments()
-#         return result
diff --git a/src/lifeblood/basenode_serialization.py b/src/lifeblood/basenode_serialization.py
index a71606cc..714c545d 100644
--- a/src/lifeblood/basenode_serialization.py
+++ b/src/lifeblood/basenode_serialization.py
@@ -1,7 +1,6 @@
 import asyncio
 from .basenode import BaseNode
 from .node_dataprovider_base import NodeDataProvider
-from .nodegraph_holder_base import NodeGraphHolderBase
 
 from typing import Iterable, List, Optional, Tuple
 
@@ -31,11 +30,11 @@ def serialize(self, node: BaseNode) -> Tuple[bytes, Optional[bytes]]:
     def serialize_state_only(self, node: BaseNode) -> Optional[bytes]:
         raise NotImplementedError()
 
-    def deserialize(self, parent: NodeGraphHolderBase, node_id: int, node_data_provider: NodeDataProvider, data: bytes, state: Optional[bytes]) -> BaseNode:
+    def deserialize(self, node_data_provider: NodeDataProvider, data: bytes, state: Optional[bytes]) -> BaseNode:
         raise NotImplementedError()
 
-    async def deserialize_async(self, parent: NodeGraphHolderBase, node_id: int, node_data_provider: NodeDataProvider, data: bytes, state: Optional[bytes]) -> BaseNode:
-        return await asyncio.get_event_loop().run_in_executor(None, self.deserialize, parent, node_id, node_data_provider, data, state)
+    async def deserialize_async(self, node_data_provider: NodeDataProvider, data: bytes, state: Optional[bytes]) -> BaseNode:
+        return await asyncio.get_event_loop().run_in_executor(None, self.deserialize, node_data_provider, data, state)
 
     async def serialize_async(self, node: BaseNode) -> Tuple[bytes, Optional[bytes]]:
         return await asyncio.get_event_loop().run_in_executor(None, self.serialize, node)
diff --git a/src/lifeblood/basenode_serializer_v1.py b/src/lifeblood/basenode_serializer_v1.py
index ebffaf04..a176bad8 100644
--- a/src/lifeblood/basenode_serializer_v1.py
+++ b/src/lifeblood/basenode_serializer_v1.py
@@ -2,12 +2,12 @@
 from io import BytesIO
 from dataclasses import dataclass
 from .basenode_serialization import NodeSerializerBase, IncompatibleDeserializationMethod
-from .basenode import BaseNode, NodeParameterType
+from .basenode import BaseNode
+from .enums import NodeParameterType
 
 from typing import Callable, Optional, Tuple, Union
 
 from .node_dataprovider_base import NodeDataProvider
-from .nodegraph_holder_base import NodeGraphHolderBase
 
 
 @dataclass
@@ -18,10 +18,9 @@ class ParameterData:
     expression: Optional[str]
 
 
-def create_node_maker(node_data_provider: NodeDataProvider) -> Callable[[str, str, NodeGraphHolderBase, int], BaseNode]:
-    def create_node(type_name: str, name: str, sched_parent, node_id) -> BaseNode:
+def create_node_maker(node_data_provider: NodeDataProvider) -> Callable[[str, str], BaseNode]:
+    def create_node(type_name: str, name: str, *args, **kwargs) -> BaseNode:  # *args, **kwargs there - for compatibility. extra args should be safely ignored
         node = node_data_provider.node_factory(type_name)(name)
-        node.set_parent(sched_parent, node_id)
         return node
     return create_node
 
@@ -30,7 +29,7 @@ class NodeSerializerV1(NodeSerializerBase):
     def serialize(self, node: BaseNode) -> Tuple[bytes, Optional[bytes]]:
         raise DeprecationWarning('no use this!')
 
-    def deserialize(self, parent: NodeGraphHolderBase, node_id: int, node_data_provider: NodeDataProvider, data: bytes, state: Optional[bytes]) -> BaseNode:
+    def deserialize(self, node_data_provider: NodeDataProvider, data: bytes, state: Optional[bytes]) -> BaseNode:
         # this be pickled
         # we do hacky things here fo backward compatibility
         class Unpickler(pickle.Unpickler):
@@ -47,5 +46,4 @@ def find_class(self, module, name):
         except Exception as e:
             raise IncompatibleDeserializationMethod(f'error loading pickle: {e}') from None
 
-        newobj.set_parent(parent, node_id)
         return newobj
diff --git a/src/lifeblood/basenode_serializer_v2.py b/src/lifeblood/basenode_serializer_v2.py
index de382a83..f30261f8 100644
--- a/src/lifeblood/basenode_serializer_v2.py
+++ b/src/lifeblood/basenode_serializer_v2.py
@@ -2,13 +2,13 @@
 import json
 from .common_serialization import AttribSerializer, AttribDeserializer
 from .basenode_serialization import NodeSerializerBase, IncompatibleDeserializationMethod, FailedToApplyNodeState, FailedToApplyParameters
-from .basenode import BaseNode, NodeParameterType
+from .basenode import BaseNode
+from .enums import NodeParameterType
 from .uidata import ParameterFullValue
 
 from typing import Optional, Tuple, Union
 
 from .node_dataprovider_base import NodeDataProvider
-from .nodegraph_holder_base import NodeGraphHolderBase
 
 
 @dataclass
@@ -85,7 +85,7 @@ def serialize_state_only(self, node: BaseNode) -> Optional[bytes]:
         state = node.get_state()
         return None if state is None else json.dumps(state, cls=NodeSerializerV2.Serializer).encode('latin1')
 
-    def deserialize(self, parent: NodeGraphHolderBase, node_id: int, node_data_provider: NodeDataProvider, data: bytes, state: Optional[bytes]) -> BaseNode:
+    def deserialize(self, node_data_provider: NodeDataProvider, data: bytes, state: Optional[bytes]) -> BaseNode:
         try:
             data_dict = json.loads(data.decode('latin1'), cls=NodeSerializerV2.Deserializer)
         except json.JSONDecodeError:
@@ -96,10 +96,11 @@ def deserialize(self, parent: NodeGraphHolderBase, node_id: int, node_data_provi
         if (fv := data_dict['format_version']) != 2:
             raise IncompatibleDeserializationMethod(f'format_version {fv} is not supported')
         new_node = node_data_provider.node_factory(data_dict['type_name'])(data_dict['name'])
-        new_node.set_parent(parent, node_id)
         try:
             with new_node.get_ui().block_ui_callbacks():
-                new_node.get_ui().set_parameters_batch({name: ParameterFullValue(val.unexpanded_value, val.expression) for name, val in data_dict['parameters'].items()})
+                param_dict = {name: ParameterFullValue(val.unexpanded_value, val.expression) for name, val in data_dict['parameters'].items()}
+                self.__resource_compatibility_filter(param_dict)  # TODO: remove this a couple of months in the future
+                new_node.get_ui().set_parameters_batch(param_dict)
         except Exception:
             # actually set_parameters_batch catches all reasonable exceptions and treats them as warnings,
             #  so this seems unreachable, but if something does happen - we treat it as fail to set all params
@@ -111,3 +112,33 @@ def deserialize(self, parent: NodeGraphHolderBase, node_id: int, node_data_provi
                 raise FailedToApplyNodeState(wrapped_expection=e)
 
         return new_node
+
+    @staticmethod
+    def __resource_compatibility_filter(param_dict: dict):
+        rename_params = {
+            'priority adjustment': '__requirements__.priority_adjustment',
+            'worker groups': '__requirements__.worker_groups',
+            'worker type': '__requirements__.worker_type',
+            'worker cpu cost': '__requirements__.f_min_res_0',
+            'worker cpu cost preferred': '__requirements__.f_pref_res_0',
+            'worker mem cost': '__requirements__.f_min_res_1',
+            'worker mem cost preferred': '__requirements__.f_pref_res_1',
+        }
+        for param_name in (
+                'worker gpu cost', 'worker gpu cost preferred', 'worker gpu mem cost', 'worker gpu mem cost preferred'
+        ):
+            if param_name in param_dict:
+                param_dict.pop(param_name)
+
+        if 'worker cpu cost' in param_dict and '__requirements__.res' not in param_dict:
+            param_dict['__requirements__.res'] = ParameterData(
+                '__requirements__.res',
+                NodeParameterType.INT,
+                2,
+                None,
+            )
+
+        for old_name, new_name in rename_params.items():
+            if new_name in param_dict or old_name not in param_dict:
+                continue
+            param_dict[new_name] = param_dict.pop(old_name)
diff --git a/src/lifeblood/core_nodes/attribute_splitter.py b/src/lifeblood/core_nodes/attribute_splitter.py
index 03800d4e..e6844041 100644
--- a/src/lifeblood/core_nodes/attribute_splitter.py
+++ b/src/lifeblood/core_nodes/attribute_splitter.py
@@ -1,5 +1,5 @@
 from copy import copy
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.enums import NodeParameterType
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 import math
diff --git a/src/lifeblood/core_nodes/del_attrib.py b/src/lifeblood/core_nodes/del_attrib.py
index cd2a2c4f..f316d251 100644
--- a/src/lifeblood/core_nodes/del_attrib.py
+++ b/src/lifeblood/core_nodes/del_attrib.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult
 from lifeblood.enums import NodeParameterType
 
diff --git a/src/lifeblood/core_nodes/environment_resolver_setter.py b/src/lifeblood/core_nodes/environment_resolver_setter.py
index b370cf28..e303d4b1 100644
--- a/src/lifeblood/core_nodes/environment_resolver_setter.py
+++ b/src/lifeblood/core_nodes/environment_resolver_setter.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNode, ProcessingError
+from lifeblood.node_plugin_base import BaseNode, ProcessingError
 from lifeblood.nodethings import ProcessingResult
 from lifeblood.processingcontext import ProcessingContext
 from lifeblood.enums import NodeParameterType
diff --git a/src/lifeblood/core_nodes/kill.py b/src/lifeblood/core_nodes/kill.py
index 331de8b5..f24b2d28 100644
--- a/src/lifeblood/core_nodes/kill.py
+++ b/src/lifeblood/core_nodes/kill.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult
 
 from typing import Iterable
diff --git a/src/lifeblood/core_nodes/mod_attrib.py b/src/lifeblood/core_nodes/mod_attrib.py
index 84fd12d9..09db58a6 100644
--- a/src/lifeblood/core_nodes/mod_attrib.py
+++ b/src/lifeblood/core_nodes/mod_attrib.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult
 from lifeblood.taskspawn import TaskSpawn
 from lifeblood.exceptions import NodeNotReadyToProcess
diff --git a/src/lifeblood/core_nodes/null.py b/src/lifeblood/core_nodes/null.py
index 9413283d..3864716e 100644
--- a/src/lifeblood/core_nodes/null.py
+++ b/src/lifeblood/core_nodes/null.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult
 
 from typing import Iterable
diff --git a/src/lifeblood/core_nodes/parent_children_waiter.py b/src/lifeblood/core_nodes/parent_children_waiter.py
index 54dc9577..6c3ce740 100644
--- a/src/lifeblood/core_nodes/parent_children_waiter.py
+++ b/src/lifeblood/core_nodes/parent_children_waiter.py
@@ -1,7 +1,7 @@
 import dataclasses
 from dataclasses import dataclass
 from lifeblood.attribute_serialization import deserialize_attributes_core
-from lifeblood.basenode import BaseNode, ProcessingError
+from lifeblood.node_plugin_base import BaseNode, ProcessingError
 from lifeblood.nodethings import ProcessingResult
 from lifeblood.taskspawn import TaskSpawn
 from lifeblood.exceptions import NodeNotReadyToProcess
diff --git a/src/lifeblood/core_nodes/python.py b/src/lifeblood/core_nodes/python.py
index a09afed8..44ea60ac 100644
--- a/src/lifeblood/core_nodes/python.py
+++ b/src/lifeblood/core_nodes/python.py
@@ -1,7 +1,7 @@
 import re
 import time
 
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
 from lifeblood.processingcontext import ProcessingContext
 from lifeblood.nodethings import ProcessingResult, ProcessingError
diff --git a/src/lifeblood/core_nodes/rename_attrib.py b/src/lifeblood/core_nodes/rename_attrib.py
index 90784fd0..05eb32e0 100644
--- a/src/lifeblood/core_nodes/rename_attrib.py
+++ b/src/lifeblood/core_nodes/rename_attrib.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult
 from lifeblood.taskspawn import TaskSpawn
 from lifeblood.exceptions import NodeNotReadyToProcess
diff --git a/src/lifeblood/core_nodes/set_attrib.py b/src/lifeblood/core_nodes/set_attrib.py
index 1b026314..cdd6de2a 100644
--- a/src/lifeblood/core_nodes/set_attrib.py
+++ b/src/lifeblood/core_nodes/set_attrib.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult
 from lifeblood.enums import NodeParameterType
 
diff --git a/src/lifeblood/core_nodes/spawn_children.py b/src/lifeblood/core_nodes/spawn_children.py
index 34db9145..ef5ae79d 100644
--- a/src/lifeblood/core_nodes/spawn_children.py
+++ b/src/lifeblood/core_nodes/spawn_children.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult
 from lifeblood.enums import NodeParameterType
 from lifeblood.taskspawn import TaskSpawn
diff --git a/src/lifeblood/core_nodes/split_waiter.py b/src/lifeblood/core_nodes/split_waiter.py
index f8f0620d..c0e863f9 100644
--- a/src/lifeblood/core_nodes/split_waiter.py
+++ b/src/lifeblood/core_nodes/split_waiter.py
@@ -1,5 +1,5 @@
 from dataclasses import dataclass
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult
 from lifeblood.taskspawn import TaskSpawn
 from lifeblood.exceptions import NodeNotReadyToProcess
diff --git a/src/lifeblood/core_nodes/switch.py b/src/lifeblood/core_nodes/switch.py
index e92a5ff2..105e0f01 100644
--- a/src/lifeblood/core_nodes/switch.py
+++ b/src/lifeblood/core_nodes/switch.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.processingcontext import ProcessingContext
 from lifeblood.enums import NodeParameterType
diff --git a/src/lifeblood/core_nodes/wait_for_task.py b/src/lifeblood/core_nodes/wait_for_task.py
index aecc8f90..7ef064ce 100644
--- a/src/lifeblood/core_nodes/wait_for_task.py
+++ b/src/lifeblood/core_nodes/wait_for_task.py
@@ -1,6 +1,6 @@
 from threading import Lock
 import shlex
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult, ProcessingContext
 from lifeblood.taskspawn import TaskSpawn
 from lifeblood.exceptions import NodeNotReadyToProcess
diff --git a/src/lifeblood/core_nodes/wedge.py b/src/lifeblood/core_nodes/wedge.py
index 2fb3199c..915de91b 100644
--- a/src/lifeblood/core_nodes/wedge.py
+++ b/src/lifeblood/core_nodes/wedge.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.enums import NodeParameterType
 from lifeblood.uidata import NodeUi, MultiGroupLayout, Parameter
diff --git a/src/lifeblood/db_misc.py b/src/lifeblood/db_misc.py
index 152cf7f2..7a5686f8 100644
--- a/src/lifeblood/db_misc.py
+++ b/src/lifeblood/db_misc.py
@@ -36,14 +36,6 @@
 );
 CREATE TABLE IF NOT EXISTS "resources" (
 	"hwid"	TEXT NOT NULL UNIQUE,
-	"cpu_count"	INTEGER NOT NULL,
-	"total_cpu_count"	INTEGER NOT NULL,
-	"cpu_mem"	INTEGER NOT NULL,
-	"total_cpu_mem"	INTEGER NOT NULL,
-	"gpu_count"	INTEGER NOT NULL,
-	"total_gpu_count"	INTEGER NOT NULL,
-	"gpu_mem"	INTEGER NOT NULL,
-	"total_gpu_mem"	INTEGER NOT NULL,
 	PRIMARY KEY("hwid")
 ) WITHOUT ROWID;
 CREATE TABLE IF NOT EXISTS "workers" (
@@ -114,7 +106,7 @@
 	PRIMARY KEY("group")
 );
 CREATE INDEX IF NOT EXISTS "task_group_attrs_state_creator_idx" ON "task_group_attributes" (
-	"status",
+	"state",
 	"creator"
 );
 CREATE INDEX IF NOT EXISTS "task_dead" ON "tasks" (
diff --git a/src/lifeblood/hardware_resources.py b/src/lifeblood/hardware_resources.py
new file mode 100644
index 00000000..f2038534
--- /dev/null
+++ b/src/lifeblood/hardware_resources.py
@@ -0,0 +1,104 @@
+import psutil
+import copy
+import re
+import json
+from .misc import get_unique_machine_id
+from .logging import get_logger
+from dataclasses import dataclass
+
+from typing import Dict, Optional, Union
+
+
+__mem_parse_re = re.compile(r'^\s*(\d+(?:\.\d+)?)\s*([BKMGTP]?)\s*$')
+
+
+def _try_parse_mem_spec(s: Union[str, int], default: Optional[int] = None):
+    if not isinstance(s, str):
+        return s
+    match = __mem_parse_re.match(s)
+    if not match:
+        get_logger('worker_resources').warning(f'could not parse "{s}", using default')
+        return default
+    bytes_count = float(match.group(1))
+    coeff = match.group(2)
+
+    coeff_map = {'B': 1,
+                 'K': 10**3,
+                 'M': 10**6,
+                 'G': 10**9,
+                 'T': 10**12,
+                 'P': 10**15}
+
+    if coeff not in coeff_map:
+        get_logger('worker_resources').warning(f'could not parse "{s}", wtf is "{coeff}"? using default')
+        return default
+
+    if coeff:
+        mult = coeff_map[coeff]
+
+        # this way we limit float op errors
+        if mult > 10**6:
+            mult //= 10**6
+            bytes_count = int(bytes_count * 10**6)
+
+        bytes_count = bytes_count * mult
+
+    return int(bytes_count)
+
+
+Number = Union[int, float]
+
+
+@dataclass
+class HardwareResource:
+    value: Number
+
+
+class HardwareResources:
+    __resource_epsilon = 1e-5
+
+    def __init__(self, *, hwid: Optional[int] = None, **resources):
+        """
+
+        NOTE: because of float resource rounding - it's not safe to rely on consistency of summing/subtracting a lot of resources
+        """
+        self.hwid = get_unique_machine_id() if hwid is None else hwid
+        self.__resources: Dict[str, HardwareResource] = {}
+        for res_name, res_val in resources.items():
+            self.__resources[res_name] = HardwareResource(res_val)
+
+    def serialize(self) -> bytes:
+        return json.dumps({
+            'hwid': self.hwid,
+            'res': {
+                name: {'value': res.value} for name, res in self.__resources.items()
+            }
+        }).encode('UTF-8')
+
+    @classmethod
+    def deserialize(cls, data_bytes: bytes) -> "HardwareResources":
+        data = json.loads(data_bytes.decode('UTF-8'))
+        return HardwareResources(hwid=data['hwid'], **{name: val_dict['value'] for name, val_dict in data['res'].items()})
+
+    def items(self):
+        return self.__resources.items()
+
+    def __iter__(self):
+        return iter(self.__resources)
+
+    def __len__(self):
+        return len(self.__resources)
+
+    def __getitem__(self, resource_name: str) -> HardwareResource:
+        return self.__resources[resource_name]
+
+    def __repr__(self):
+        parts = []
+        for res_name, res in self.__resources.items():
+            parts.append(f'{res_name}: {res.value}')
+        return f'<hwid={self.hwid}, {", ".join(parts)}>'
+
+    def __eq__(self, other):
+        if not isinstance(other, HardwareResources):
+            return False
+        return self.hwid == other.hwid and self.__resources == other.__resources
diff --git a/src/lifeblood/invocationjob.py b/src/lifeblood/invocationjob.py
index ddfa6aa6..8d441719 100644
--- a/src/lifeblood/invocationjob.py
+++ b/src/lifeblood/invocationjob.py
@@ -1,17 +1,21 @@
 import os
 import re
 from copy import copy, deepcopy
+import json
 import asyncio
 import pickle
 from types import MappingProxyType
 from .enums import WorkerType
-from .net_classes import WorkerResources
+from dataclasses import dataclass, field
 
 from typing import Optional, Iterable, Union, Dict, List, Set, Tuple, TYPE_CHECKING
 if TYPE_CHECKING:
     from .environment_resolver import EnvironmentResolverArguments
 
 
+Number = Union[int, float]
+
+
 class InvocationNotFinished(RuntimeError):
     pass
 
@@ -92,10 +96,6 @@ def _enqueue_kv_method(self, method: str, key: str, value):
             value = str(value)
         self.__action_queue.append((method, key, value))
 
-    # def __getattr__(self, item):
-    #     return lambda k, v: self._enqueue_kv_method(item, k, v)
-
-    # these 2 guys are explicitly added only for IDE popup hints
     def prepend(self, key: str, value):
         self._enqueue_kv_method('prepend', key, value)
 
@@ -103,6 +103,67 @@ def append(self, key: str, value):
         self._enqueue_kv_method('append', key, value)
 
 
+@dataclass
+class ResourceRequirement:
+    min: Number = 0
+    pref: Number = 0
+
+
+class ResourceRequirements:
+    """
+    requirements for only resources
+    """
+    def __init__(self, data: Optional[Dict[str, ResourceRequirement]] = None):
+        if data:
+            self.__res: Dict[str, ResourceRequirement] = data
+        else:
+            self.__res: Dict[str, ResourceRequirement] = {}
+
+    def get(self, name: str, default_val=None):
+        return self.__res.get(name, default_val)
+
+    def __getitem__(self, name: str) -> ResourceRequirement:
+        """
+        get requirements for resource "name"
+        """
+        return self.__res[name]
+
+    def __setitem__(self, name: str, value: ResourceRequirement):
+        self.__res[name] = value
+
+    def __contains__(self, name):
+        return name in self.__res
+
+    def items(self):
+        return self.__res.items()
+
+
+@dataclass
+class Requirements:
+    resources: ResourceRequirements = field(default_factory=ResourceRequirements)
+    # devices:   TO ADD LATER
+
+    def serialize_to_string(self):
+        """
+        compact string representation
+        """
+        return json.dumps({
+            'r': {name: {'m': val.min, 'p': val.pref} for name, val in self.resources.items()}
+        })
+
+    @classmethod
+    def deserialize_from_string(cls, text: str) -> "Requirements":
+        """
+        reverse from serializa_to_stirng
+        """
+        data = json.loads(text)
+        return Requirements(
+            resources=ResourceRequirements({
+                name: ResourceRequirement(val['m'], val['p']) for name, val in data['r'].items()
+            })
+        )
+
+
 class InvocationRequirements:
     """
     requirements a worker has to match in order to be able to pick this task
@@ -112,56 +173,49 @@ class InvocationRequirements:
     then a 1 core machine may pick up one task, but 16 core machine will pick just 4 tasks
     """
     def __init__(self, *,
-                 min_cpu_count: Optional[int] = None,
-                 min_memory_bytes: Optional[int] = None,
-                 min_gpu_count: Optional[int] = None,
-                 min_gpu_memory_bytes: Optional[int] = None,
                  groups: Optional[Iterable[str]] = None,
-                 worker_type: WorkerType = WorkerType.STANDARD):
+                 worker_type: WorkerType = WorkerType.STANDARD,
+                 **resources):
+        """
+        each arg in **resources must start with "min_<name>" or "pref_<name>",
+        where <name> is the name of the resource
+        """
         self.__groups = set(groups) if groups is not None else set()
         self.__worker_type = worker_type
-        self.__min_cpu_count = min_cpu_count or 0
-        self.__min_memory_bytes = min_memory_bytes or 0
-        self.__min_gpu_count = min_gpu_count or 0
-        self.__min_gpu_memory_bytes = min_gpu_memory_bytes or 0
 
-        self.__pref_cpu_count = None
-        self.__pref_memory_bytes = None
-        self.__pref_gpu_count = None
-        self.__pref_gpu_memory_bytes = None
+        self.__res_req: Requirements = Requirements()  # Dict[str, ResourceRequirement] = {}
+        for arg_name, arg_val in resources.items():
+            if '_' not in arg_name:
+                raise RuntimeError(f'provided resource {arg_name} must start with either "min_", or "pref_"')
+            bound, res = arg_name.split(arg_name, 1)
+
+            if res not in self.__res_req.resources:
+                self.__res_req.resources[res] = ResourceRequirement()
+            if bound == 'min':
+                self.__res_req.resources[res].min = arg_val
+            elif bound == 'pref':
+                self.__res_req.resources[res].pref = arg_val
+            else:
+                raise RuntimeError(f'provided resource {arg_name} must start with either "min_", or "pref_"')
 
     # querries
 
     def groups(self) -> Set[str]:
         return set(self.__groups)
 
-    def min_cpu_count(self) -> Union[float, int]:
-        return self.__min_cpu_count
-
-    def min_memory_bytes(self) -> int:
-        return self.__min_memory_bytes
-
-    def preferred_cpu_count(self) -> Union[float, int]:
-        return self.__pref_cpu_count
-
-    def preferred_memory_bytes(self) -> int:
-        return self.__pref_memory_bytes
-
-    def min_gpu_count(self) -> Union[float, int]:
-        return self.__min_gpu_count
-
-    def min_gpu_memory_bytes(self) -> int:
-        return self.__min_gpu_memory_bytes
-
-    def preferred_gpu_count(self) -> Union[float, int]:
-        return self.__pref_gpu_count
-
-    def preferred_gpu_memory_bytes(self) -> int:
-        return self.__pref_gpu_memory_bytes
-
     def worker_type(self) -> WorkerType:
         return self.__worker_type
 
+    def min_resource(self, resource_name: str) -> Union[float, int]:
+        if resource_name not in self.__res_req.resources:
+            return 0
+        return self.__res_req.resources[resource_name].min
+
+    def preferred_resource(self, resource_name: str) -> Union[float, int]:
+        if resource_name not in self.__res_req.resources:
+            return 0
+        return self.__res_req.resources[resource_name].pref
+
     # setters
 
     def set_groups(self, groups):
@@ -173,43 +227,23 @@ def add_groups(self, groups):
     def add_group(self, group: str):
         self.__groups.add(group)
 
-    def set_min_cpu_count(self, min_cpu_count):
-        self.__min_cpu_count = min_cpu_count
-
-    def set_min_memory_bytes(self, min_memory_bytes):
-        self.__min_memory_bytes = int(min_memory_bytes)
-
-    def set_preferred_cpu_count(self, pref_cpu_count):
-        self.__pref_cpu_count = pref_cpu_count
-
-    def set_preferred_memory_bytes(self, pref_memory_bytes):
-        self.__pref_memory_bytes = int(pref_memory_bytes)
-
-    def set_min_gpu_count(self, min_gpu_count):
-        self.__min_gpu_count = min_gpu_count
-
-    def set_min_gpu_memory_bytes(self, min_gpu_memory_bytes):
-        self.__min_gpu_memory_bytes = int(min_gpu_memory_bytes)
+    def set_min_resource(self, resource_name: str, value: Union[float, int]):
+        if resource_name not in self.__res_req.resources:
+            self.__res_req.resources[resource_name] = ResourceRequirement()
+        self.__res_req.resources[resource_name].min = value
 
-    def set_preferred_gpu_count(self, pref_gpu_count):
-        self.__pref_gpu_count = pref_gpu_count
-
-    def set_preferred_gpu_memory_bytes(self, pref_gpu_memory_bytes):
-        self.__pref_gpu_memory_bytes = int(pref_gpu_memory_bytes)
+    def set_preferred_resource(self, resource_name: str, value: Union[float, int]):
+        if resource_name not in self.__res_req.resources:
+            self.__res_req.resources[resource_name] = ResourceRequirement()
+        self.__res_req.resources[resource_name].pref = value
 
     def set_worker_type(self, worker_type: WorkerType):
         self.__worker_type = worker_type
 
     def final_where_clause(self):
         conds = [f'("worker_type" = {self.__worker_type.value})']
-        if self.__min_cpu_count > 0:
-            conds.append(f'("cpu_count" >= {self.__min_cpu_count - 1e-8 if isinstance(self.__min_cpu_count, float) else self.__min_cpu_count})')   # to ensure sql compare will work
-        if self.__min_memory_bytes > 0:
-            conds.append(f'("cpu_mem" >= {self.__min_memory_bytes})')
-        if self.__min_gpu_count > 0:
-            conds.append(f'("gpu_count" >= {self.__min_gpu_count - 1e-8 if isinstance(self.__min_gpu_count, float) else self.__min_gpu_count})')  # to ensure sql compare will work
-        if self.__min_gpu_memory_bytes > 0:
-            conds.append(f'("gpu_mem" >= {self.__min_gpu_memory_bytes})')
+        for res_name, res_req in self.__res_req.resources.items():
+            conds.append(f'("{res_name}" >= {res_req.min - 1e-8 if isinstance(res_req.min, float) else res_req.min})')  # to ensure sql compare will work
         if len(self.__groups) > 0:
             esc = '\\'
 
@@ -218,50 +252,25 @@ def _morph(s: str):
             conds.append(f'''(EXISTS (SELECT * FROM worker_groups wg WHERE wg."worker_hwid" == workers."hwid" AND ( {" OR ".join(f"""wg."group" LIKE '{_morph(x)}' ESCAPE '{esc}'""" for x in self.__groups)} )))''')
         return ' AND '.join(conds)
 
-    def to_dict(self, resources_only: bool = False) -> dict:
-        ret = {'cpu_count': self.__min_cpu_count,
-               'cpu_mem': self.__min_memory_bytes,
-               'gpu_count': self.__min_gpu_count,
-               'gpu_mem': self.__min_gpu_memory_bytes}
-        if self.__pref_cpu_count is not None:
-            ret['pref_cpu_count'] = self.__pref_cpu_count
-        if self.__pref_memory_bytes is not None:
-            ret['pref_cpu_mem'] = self.__pref_memory_bytes
-        if self.__pref_gpu_count is not None:
-            ret['pref_gpu_count'] = self.__pref_gpu_count
-        if self.__pref_gpu_memory_bytes is not None:
-            ret['pref_gpu_mem'] = self.__pref_gpu_memory_bytes
-
-        if resources_only:
-            return ret
-
-        ret['groups'] = tuple(self.groups())
-        return ret
-
-    def to_min_worker_resources(self) -> WorkerResources:
-        res = WorkerResources()
-        res.cpu_count = self.__min_cpu_count
-        res.cpu_mem = self.__min_memory_bytes
-        res.gpu_count = self.__min_gpu_count
-        res.gpu_mem = self.__min_gpu_memory_bytes
-        return res
-
-    def __gt__(self, other):
-        if not isinstance(other, WorkerResources):
-            raise NotImplementedError()
-        return self.__min_cpu_count > other.cpu_count and \
-               self.__min_memory_bytes > other.cpu_mem and \
-               self.__min_gpu_count > other.gpu_count and \
-               self.__min_gpu_memory_bytes > other.gpu_mem
-
-    def __eq__(self, other):
-        if not isinstance(other, WorkerResources):
-            raise NotImplementedError()
-        return self.__min_cpu_count == other.cpu_count and \
-               self.__min_memory_bytes == other.cpu_mem and \
-               self.__min_gpu_count == other.gpu_count and \
-               self.__min_gpu_memory_bytes == other.gpu_mem
+    def pack_selection_info(self) -> str:
+        """
+        provides a string with enough info to select worker and update worker resource
+        this is to be stored in SQLite db
+        """
+        return ':::'.join((self.final_where_clause(), self.__res_req.serialize_to_string()))
 
+    @classmethod
+    def unpack_selection_info(cls, packed_string: str) -> Tuple[str, Requirements]:
+        """
+        returns sql clause  and  requirements
+        reverse of what pack_selection_info() does
+        """
+        splitpos = packed_string.rfind(':::')
+        if splitpos < 0:
+            raise ValueError(f'malformed argument string: {repr(packed_string)}')
+        requirements_clause = Requirements.deserialize_from_string(packed_string[splitpos + 3:])
+        requirements_clause_sql = packed_string[:splitpos]
+        return requirements_clause_sql, requirements_clause
 
 class InvocationJob:
     """
diff --git a/src/lifeblood/main_scheduler.py b/src/lifeblood/main_scheduler.py
index df73d68e..461eb356 100644
--- a/src/lifeblood/main_scheduler.py
+++ b/src/lifeblood/main_scheduler.py
@@ -1,6 +1,5 @@
 import sys
 import os
-import re
 from pathlib import Path
 import asyncio
 import signal
@@ -8,75 +7,53 @@
 from .scheduler import Scheduler
 from .basenode_serializer_v1 import NodeSerializerV1
 from .basenode_serializer_v2 import NodeSerializerV2
-from .defaults import scheduler_port as default_scheduler_port, ui_port as default_ui_port
-from .config import get_config, create_default_user_config_file, get_local_scratch_path
+from .scheduler_config_provider_base import SchedulerConfigProviderBase
+from .scheduler_config_provider_file import SchedulerConfigProviderFileOverrides
 from . import logging
-from . import paths
-from .text import escape
-
-from typing import Optional, Tuple
-
-
-__esc = '\\"'
-
-default_config = f'''
-[core]
-## you can uncomment stuff below to specify some static values
-## 
-# server_ip = "192.168.0.2"
-# server_port = {default_scheduler_port()}
-# ui_ip = "192.168.0.2"
-# ui_port = {default_ui_port()}
-
-## you can turn off scheduler broadcasting if you want to manually configure viewer and workers to connect
-## to a specific address
-# broadcast = false
-
-[scheduler]
-
-[scheduler.globals]
-## entries from this section will be available to any node from config[key] 
-##
-## if you use more than 1 machine - you must change this to a network location shared among all workers
-## by default it's set to scheduler's machine local temp path, and will only work for 1 machine setup 
-global_scratch_location = "{escape(get_local_scratch_path(), __esc)}"
-
-[scheduler.database]
-## you can specify default database path, 
-##  but this can be overriden with command line argument --db-path
-# path = "/path/to/database.db"
-
-## uncomment line below to store task logs outside of the database
-##  it works in a way that all NEW logs will be saved according to settings below
-##  existing logs will be kept where they are
-##  external logs will ALWAYS be looked for in location specified by store_logs_externally_location
-##  so if you have ANY logs saved externally - you must keep store_logs_externally_location defined in the config, 
-##    or those logs will be inaccessible
-##  but you can safely move logs and change location in config accordingly, but be sure scheduler is not accessing them at that time
-# store_logs_externally = true
-# store_logs_externally_location = /path/to/dir/where/to/store/logs
-'''
-
-
-def create_default_scheduler(db_file_path, *,
-                 do_broadcasting: Optional[bool] = None,
-                 broadcast_interval: Optional[int] = None,
-                 helpers_minimal_idle_to_ensure=1,
-                 server_addr: Optional[Tuple[str, int, int]] = None,
-                 server_ui_addr: Optional[Tuple[str, int]] = None) -> Scheduler:
-    return Scheduler(
-        db_file_path,
-        node_data_provider=PluginNodeDataProvider(),
-        node_serializers=[NodeSerializerV2(), NodeSerializerV1()],
-        do_broadcasting=do_broadcasting,
-        broadcast_interval=broadcast_interval,
-        helpers_minimal_idle_to_ensure=helpers_minimal_idle_to_ensure,
-        server_addr=server_addr,
-        server_ui_addr=server_ui_addr,
-    )
 
-
-async def main_async(db_path=None, *, broadcast_interval: Optional[int] = None):
+from typing import Iterable, List, Optional, Tuple, Union
+
+
+def __construct_plugin_paths(custom_plugins_path: Union[None, str, Path], plugin_search_locations: Iterable[Union[str, Path]]) -> List[Tuple[Path, str]]:
+    logger = logging.get_logger('scheduler')
+    plugin_paths: List[Tuple[Path, str]] = []  # list of tuples of path to dir, plugin category
+    core_plugins_path = Path(__file__).parent / 'core_nodes'
+    stock_plugins_path = Path(__file__).parent / 'stock_nodes'
+
+    # "custom" path always comes first, as earlier entries take precedence in case of conflicts
+    if custom_plugins_path is not None:
+        if isinstance(custom_plugins_path, str):
+            custom_plugins_path = Path(custom_plugins_path)
+        if not custom_plugins_path.is_absolute():
+            custom_plugins_path = custom_plugins_path.absolute()
+        # create dir for the "custom package". all changes (preset/settings creation) BY DEFAULT go into this package
+        (custom_plugins_path / 'custom_default').mkdir(parents=True, exist_ok=True)
+        plugin_paths.append((custom_plugins_path, 'user'))
+
+    # user defined packages come next
+    extra_paths = []
+    for path in plugin_search_locations:
+        if isinstance(path, str):
+            path = Path(path)
+        if not path.is_absolute():
+            logger.warning(f'"{path}" is not absolute, skipping')
+            continue
+        if not path.exists():
+            logger.warning(f'"{path}" does not exist, skipping')
+            continue
+        extra_paths.append(path)
+        logger.debug(f'using extra plugin path: "{path}"')
+
+    plugin_paths.extend((x, 'extra') for x in extra_paths)
+
+    # then core and stock come as the baseline
+    plugin_paths.append((stock_plugins_path, 'stock'))
+    plugin_paths.append((core_plugins_path, 'core'))
+
+    return plugin_paths
+
+
+async def main_async(config: SchedulerConfigProviderBase):
     def graceful_closer(*args):
         scheduler.stop()
 
@@ -90,11 +67,17 @@ async def windows_graceful_closer():
             await asyncio.sleep(1)
         graceful_closer()
 
-    scheduler = create_default_scheduler(
-        db_path,
-        do_broadcasting=broadcast_interval > 0 if broadcast_interval is not None else None,
-        broadcast_interval=broadcast_interval
+    scheduler = Scheduler(
+        scheduler_config_provider=config,
+        node_data_provider=PluginNodeDataProvider(
+            plugin_paths=__construct_plugin_paths(
+                custom_plugins_path=config.node_data_provider_custom_plugins_path(),
+                plugin_search_locations=config.node_data_provider_extra_plugin_paths(),
+            ),
+        ),
+        node_serializers=[NodeSerializerV2(), NodeSerializerV1()],
     )
+
     win_signal_waiting_task = None
     try:
         asyncio.get_event_loop().add_signal_handler(signal.SIGINT, graceful_closer)
@@ -124,16 +107,11 @@ def main(argv):
     opts = parser.parse_args(argv)
 
     # check and create default config if none
-    create_default_user_config_file('scheduler', default_config)
-
-    config = get_config('scheduler')
-    if opts.db_path is not None:
-        db_path = opts.db_path
-    else:
-        db_path = config.get_option_noasync('scheduler.database.path', str(paths.default_main_database_location()))
+    SchedulerConfigProviderFileOverrides.generate_default_config_file_if_needed()
 
     global_logger = logging.get_logger('scheduler')
 
+    db_path = opts.db_path
     fd = None
     if opts.ephemeral:
         if opts.db_path is not None:
@@ -158,10 +136,16 @@ def main(argv):
 
         fd, db_path = tempfile.mkstemp(dir=lb_shm_path, prefix='shedb-')
 
+    config = SchedulerConfigProviderFileOverrides(
+        main_db_location=db_path,
+        do_broadcast=opts.broadcast_interval > 0 if opts.broadcast_interval is not None else None,
+        broadcast_interval=opts.broadcast_interval,
+    )
+
     if opts.verbosity_pinger:
         logging.get_logger('scheduler.worker_pinger').setLevel(opts.verbosity_pinger)
     try:
-        asyncio.run(main_async(db_path, broadcast_interval=opts.broadcast_interval))
+        asyncio.run(main_async(config))
     except KeyboardInterrupt:
         global_logger.warning('SIGINT caught')
         global_logger.info('SIGINT caught. Scheduler is stopped now.')
diff --git a/src/lifeblood/net_classes.py b/src/lifeblood/net_classes.py
deleted file mode 100644
index d54bbb9f..00000000
--- a/src/lifeblood/net_classes.py
+++ /dev/null
@@ -1,152 +0,0 @@
-import uuid
-import psutil
-import pickle
-import copy
-import re
-from .misc import get_unique_machine_id
-from .logging import get_logger
-
-from typing import Optional, Union
-
-__logger = get_logger('worker_resources')
-
-
-__mem_parse_re = re.compile(r'^\s*(\d+(?:\.\d+)?)\s*([BKMGTP]?)\s*$')
-
-
-def _try_parse_mem_spec(s: Union[str, int], default: Optional[int] = None):
-    if not isinstance(s, str):
-        return s
-    match = __mem_parse_re.match(s)
-    if not match:
-        __logger.warning(f'could not parse "{s}", using default')
-        return default
-    bytes_count = float(match.group(1))
-    coeff = match.group(2)
-
-    coeff_map = {'B': 1,
-                 'K': 10**3,
-                 'M': 10**6,
-                 'G': 10**9,
-                 'T': 10**12,
-                 'P': 10**15}
-
-    if coeff not in coeff_map:
-        __logger.warning(f'could not parse "{s}", wtf is "{coeff}"? using default')
-        return default
-
-    if coeff:
-        mult = coeff_map[coeff]
-
-        # this way we limit float op errors
-        if mult > 10**6:
-            mult //= 10**6
-            bytes_count = int(bytes_count * 10**6)
-
-        bytes_count = bytes_count * mult
-
-    return int(bytes_count)
-
-
-class WorkerResources:
-    __res_names = ('cpu_count', 'cpu_mem', 'gpu_count', 'gpu_mem')  # name of all main resources
-    __resource_epsilon = 1e-5
-
-    def __init__(self,
-                 cpu_count: Union[int, float, None] = None, cpu_mem: Optional[int] = None,
-                 gpu_count: Union[int, float, None] = None, gpu_mem: Optional[int] = None):
-        """
-
-        NOTE: because of float resource rounding - it's not safe to rely on consistency of summing/subtracting a lot of resources
-        """
-        self.hwid = get_unique_machine_id()
-        self.cpu_count = cpu_count if cpu_count is not None else psutil.cpu_count()  # note, cpu/gpu count can be float by design, but we don't like "almost" round float values
-        self.cpu_mem = _try_parse_mem_spec(cpu_mem) if cpu_mem is not None else psutil.virtual_memory().total
-        self.gpu_count = gpu_count if gpu_count is not None else 0  # TODO: implement GPU autodetection
-        self.gpu_mem = _try_parse_mem_spec(gpu_mem) if gpu_mem is not None else 0
-        self.total_cpu_count = self.cpu_count
-        self.total_cpu_mem = self.cpu_mem
-        self.total_gpu_count = self.gpu_count
-        self.total_gpu_mem = self.gpu_mem
-
-        for rname in self.__res_names:  # sanity check
-            assert hasattr(self, rname)
-            assert hasattr(self, f'total_{rname}')
-
-    def serialize(self) -> bytes:
-        return pickle.dumps(self)
-
-    @classmethod
-    def deserialize(cls, data) -> "WorkerResources":
-        return pickle.loads(data)
-
-    def is_valid(self):
-        return all(getattr(self, res) >= 0 for res in self.__res_names)
-        # return self.cpu_count >= 0 and \
-        #        self.cpu_mem >= 0 and \
-        #        self.gpu_count >= 0 and \
-        #        self.gpu_mem >= 0
-
-    def __repr__(self):
-        return f'<cpu: {self.cpu_count}/{self.total_cpu_count}, mem: {self.cpu_mem}/{self.total_cpu_mem}, gpu: {self.gpu_count}/{self.total_gpu_count}, gmm: {self.gpu_mem}/{self.total_gpu_mem}>'
-
-    def __lt__(self, other):
-        if not isinstance(other, WorkerResources):
-            return other > self
-        return any(getattr(self, res) < getattr(other, res) for res in self.__res_names)
-        # return self.cpu_count < other.cpu_count or \
-        #        self.cpu_mem < other.cpu_mem or \
-        #        self.gpu_count < other.gpu_count or \
-        #        self.gpu_mem < other.gpu_mem
-
-    def __eq__(self, other):
-        """
-        note - this does NOT compare totals, only actual resources
-        :param other:
-        :return:
-        """
-        if not isinstance(other, WorkerResources):
-            return other == self
-        return all(getattr(self, res) == getattr(other, res) for res in self.__res_names)
-        # return self.cpu_count == other.cpu_count and \
-        #        self.cpu_mem == other.cpu_mem and \
-        #        self.gpu_count == other.gpu_count and \
-        #        self.gpu_mem == other.gpu_mem
-
-    def __ne__(self, other):
-        return not (self == other)
-
-    def __le__(self, other):
-        return self < other or self == other
-
-    def __sub__(self, other):
-        res = copy.copy(self)
-        for resname in self.__res_names:
-            val = getattr(res, resname) - getattr(other, resname)
-            if isinstance(val, float):
-                rounded_val = round(val)
-                if abs(val - rounded_val) <= self.__resource_epsilon:
-                    val = rounded_val
-            setattr(res, resname, val)
-        # res.cpu_count -= other.cpu_count
-        # res.cpu_mem -= other.cpu_mem
-        # res.gpu_count -= other.gpu_count
-        # res.gpu_mem -= other.gpu_mem
-        return res
-
-    def __add__(self, other):
-        res = copy.copy(self)
-        for resname in self.__res_names:
-            val = getattr(res, resname) - getattr(other, resname)
-            if isinstance(val, float):
-                rounded_val = round(val)
-                if abs(val - rounded_val) <= self.__resource_epsilon:
-                    val = rounded_val
-            setattr(res, resname, val)
-        # res.cpu_count += other.cpu_count
-        # res.cpu_mem += other.cpu_mem
-        # res.gpu_count += other.gpu_count
-        # res.gpu_mem += other.gpu_mem
-        return res
-
-
diff --git a/src/lifeblood/node_dataprovider_base.py b/src/lifeblood/node_dataprovider_base.py
index 2e390e71..700670a4 100644
--- a/src/lifeblood/node_dataprovider_base.py
+++ b/src/lifeblood/node_dataprovider_base.py
@@ -6,6 +6,12 @@
 
 
 class NodeDataProvider:
+    """
+    Node Data Provider is responsible for providing node-related data.
+    It can use config providers to figure out where to search for node data,
+    but ultimately it is this class's responsibility to load/save all node-related data,
+    even though some things may fall into gray area between the two.
+    """
     def node_settings_names(self, type_name: str) -> Set[str]:
         raise NotImplementedError()
 
@@ -34,8 +40,11 @@ def node_preset_names(self, package_name: str) -> Set[str]:
     def node_preset(self, package_name: str, preset_name: str) -> NodeSnippetData:
         raise NotImplementedError()
 
+    def loaded_packages_paths(self) -> Tuple[Path, ...]:
+        raise NotImplementedError()
+
     def add_settings_to_existing_package(self, package_name_or_path: Union[str, Path], node_type_name: str, settings_name: str, settings: Dict[str, Any]):
         raise NotImplementedError()
 
-    def set_settings_as_default(self, node_type_name: str, settings_name: Optional[str]):
+    def set_settings_as_default_in_existing_package(self, package_name_or_path: Union[str, Path], node_type_name: str, settings_name: Optional[str]):
         raise NotImplementedError()
diff --git a/src/lifeblood/node_plugin_base.py b/src/lifeblood/node_plugin_base.py
new file mode 100644
index 00000000..1e61300e
--- /dev/null
+++ b/src/lifeblood/node_plugin_base.py
@@ -0,0 +1,126 @@
+"""
+This high-level module has base classes to be used by plugin nodes instead of directly using BaseNode from lifeblood.basenode
+"""
+
+import re
+from .basenode import BaseNode
+from .enums import NodeParameterType, WorkerType
+from .nodegraph_holder_base import NodeGraphHolderBase
+from .processingcontext import ProcessingContext
+from .nodethings import ProcessingResult, ProcessingError  # unused import - for easy reexport to plugins
+from .worker_resource_definition import WorkerResourceDefinition, WorkerResourceDataType
+from .uidata import NodeUi
+from .scheduler.scheduler import Scheduler
+
+from typing import Dict
+
+
+class BaseNodeWithTaskRequirements(BaseNode):
+    def __init__(self, name: str):
+        super(BaseNodeWithTaskRequirements, self).__init__(name)
+        self.__res_definitions: Dict[str, WorkerResourceDefinition] = {}
+
+        ui = self.get_ui()
+        with ui.initializing_interface_lock():
+            with ui.collapsable_group_block('__requirements__', 'worker requirements'):
+                ui.add_parameter('__requirements__.priority_adjustment', 'priority adjustment', NodeParameterType.FLOAT, 0).set_slider_visualization(-100, 100)
+                with ui.multigroup_parameter_block('__requirements__.res', 'Resources'):
+                    with ui.parameters_on_same_line_block():
+                        name_param = ui.add_parameter('__requirements__.name_res', None, NodeParameterType.STRING, 'res')
+                        ui.add_parameter('__requirements__.label_res', None, NodeParameterType.STRING, 'label')
+                        type_param = ui.add_parameter('__requirements__.type_res', None, NodeParameterType.INT, 0, can_have_expressions=False)
+                        name_param.set_hidden(True)
+                        type_param.set_hidden(True)
+                        ui.add_parameter('__requirements__.f_min_res', 'min <> preferred', NodeParameterType.FLOAT, 0.0) \
+                            .set_value_limits(value_min=0) \
+                            .append_visibility_condition(type_param, '==', 0)
+                        ui.add_parameter('__requirements__.f_pref_res', None, NodeParameterType.FLOAT, 0.0) \
+                            .set_value_limits(value_min=0) \
+                            .append_visibility_condition(type_param, '==', 0)
+                        ui.add_parameter('__requirements__.i_min_res', 'min <> preferred', NodeParameterType.INT, 0) \
+                            .set_value_limits(value_min=0) \
+                            .append_visibility_condition(type_param, '==', 1)
+                        ui.add_parameter('__requirements__.i_pref_res', None, NodeParameterType.INT, 0) \
+                            .set_value_limits(value_min=0) \
+                            .append_visibility_condition(type_param, '==', 1)
+                ui.parameter('__requirements__.res').set_hidden(True)
+                ui.add_parameter('__requirements__.worker_groups', 'groups (space or comma separated)', NodeParameterType.STRING, '')
+                ui.add_parameter('__requirements__.worker_type', 'worker type', NodeParameterType.INT, WorkerType.STANDARD.value)\
+                    .add_menu((('standard', WorkerType.STANDARD.value),
+                               ('scheduler helper', WorkerType.SCHEDULER_HELPER.value)))
+
+    def __check_set(self, param_name: str, value):
+        # NOTE: no context here, expressions are NOT expected (must be forbidden) in these parameters
+        param = self.param(param_name)
+        if param.value() == value and param.is_locked():
+            return
+        param.set_locked(False)
+        param.set_value(value)
+        param.set_locked(True)
+
+    def set_parent(self, graph_holder: NodeGraphHolderBase, node_id_in_graph: int):
+        # inherit resource definitions from newly attached parent
+        self.__res_definitions = {}
+        if isinstance(graph_holder, Scheduler):
+            config_provider = graph_holder.config_provider
+            # TODO: store existing values and set them after to preserve when defs chane
+            resource_defs = config_provider.hardware_resource_definitions()
+            self.__res_definitions = {x.name: x for x in resource_defs}
+
+            self.__check_set('__requirements__.res', len(resource_defs))
+            for i, res_def in enumerate(resource_defs):
+                self.__check_set(f'__requirements__.name_res_{i}', res_def.name)
+                self.__check_set(f'__requirements__.label_res_{i}', res_def.label or res_def.name)
+                if res_def.type in (WorkerResourceDataType.GENERIC_FLOAT, WorkerResourceDataType.SHARABLE_COMPUTATIONAL_UNIT, WorkerResourceDataType.MEMORY_BYTES):
+                    self.__check_set(f'__requirements__.type_res_{i}', 0)
+                elif res_def.type in (WorkerResourceDataType.GENERIC_INT,):
+                    self.__check_set(f'__requirements__.type_res_{i}', 1)
+                else:
+                    raise NotImplementedError(f'unknown resource data type "{res_def.type}"')
+        #
+        super().set_parent(graph_holder, node_id_in_graph)
+
+    def __apply_requirements(self, task_dict: dict, node_config: dict, result: ProcessingResult):
+        if result.invocation_job is not None:
+            context = ProcessingContext(self, task_dict, node_config)
+            raw_groups = context.param_value('__requirements__.worker_groups').strip()
+            reqs = result.invocation_job.requirements()
+            if raw_groups != '':
+                reqs.add_groups(re.split(r'[ ,]+', raw_groups))
+
+            for res_i in range(context.param_value('__requirements__.res')):
+                res_name = context.param_value(f'__requirements__.name_res_{res_i}')
+                res_type = context.param_value(f'__requirements__.type_res_{res_i}')
+                if res_type == 0:  # Float
+                    res_min = context.param_value(f'__requirements__.f_min_res_{res_i}')
+                    res_pref = context.param_value(f'__requirements__.f_pref_res_{res_i}')
+                elif res_type == 1:  # Int
+                    res_min = context.param_value(f'__requirements__.i_min_res_{res_i}')
+                    res_pref = context.param_value(f'__requirements__.i_pref_res_{res_i}')
+                else:
+                    raise NotImplementedError(f'unknown resource type value: {res_type}')
+                if res_def := self.__res_definitions.get(res_name):
+                    if res_def.type == WorkerResourceDataType.MEMORY_BYTES:
+                        # user inputs memory in float GBs
+                        res_min = int(res_min * 10**9)
+                        res_pref = int(res_pref * 10 ** 9)
+                reqs.set_min_resource(
+                    res_name,
+                    res_min
+                )
+                reqs.set_preferred_resource(
+                    res_name,
+                    res_pref
+                )
+            reqs.set_worker_type(WorkerType(context.param_value('__requirements__.worker_type')))
+            result.invocation_job.set_requirements(reqs)
+            result.invocation_job.set_priority(context.param_value('__requirements__.priority_adjustment'))
+        return result
+
+    def _process_task_wrapper(self, task_dict, node_config) -> ProcessingResult:
+        result = super(BaseNodeWithTaskRequirements, self)._process_task_wrapper(task_dict, node_config)
+        return self.__apply_requirements(task_dict, node_config, result)
+
+    def _postprocess_task_wrapper(self, task_dict, node_config) -> ProcessingResult:
+        result = super(BaseNodeWithTaskRequirements, self)._postprocess_task_wrapper(task_dict, node_config)
+        return self.__apply_requirements(task_dict, node_config, result)
diff --git a/src/lifeblood/pluginloader.py b/src/lifeblood/pluginloader.py
index f3fcb258..a2e768b8 100644
--- a/src/lifeblood/pluginloader.py
+++ b/src/lifeblood/pluginloader.py
@@ -10,22 +10,21 @@
 from .basenode import BaseNode
 from .node_dataprovider_base import NodeDataProvider
 from .snippets import NodeSnippetData
-from . import logging, plugin_info, paths
+from . import logging, plugin_info
 
-from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union, Set
+from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union, Set, Sequence
 
 
 class PluginNodeDataProvider(NodeDataProvider):
     __instance = None
 
-    @classmethod
-    def instance(cls):
-        if cls.__instance is None:
-            cls.__instance = PluginNodeDataProvider()
-        return cls.__instance
-
-    def __init__(self):
+    def __init__(self, plugin_paths: Sequence[Tuple[Path, str]]):
+        """
+        Plugin Node Data Provider will search for plugins in given locations.
+        """
         if self.__instance is not None:
+            # TODO: not very nice design, since it modifies os.environ,
+            #  so for now i force single instance, but this needs refactoring!
             raise RuntimeError("cannot have more than one PluginNodeDataProvider instance, as it manages global state")
 
         self.__plugins = {}
@@ -48,59 +47,31 @@ def __init__(self):
 
         self.logger.info('loading core plugins')
         self.__plugins = {}
-        plugin_paths: List[Tuple[str, str]] = []  # list of tuples of path to dir, plugin category
-        core_plugins_path = os.path.join(os.path.dirname(__file__), 'core_nodes')
-        stock_plugins_path = os.path.join(os.path.dirname(__file__), 'stock_nodes')
-        custom_plugins_path = paths.config_path('', 'custom_plugins')
-        plugin_paths.append((core_plugins_path, 'core'))
-        plugin_paths.append((stock_plugins_path, 'stock'))
-        (custom_plugins_path/'custom_default').mkdir(parents=True, exist_ok=True)
-
-        plugin_paths.append((str(custom_plugins_path), 'user'))
-
-        extra_paths = []
-        for path in os.environ.get('LIFEBLOOD_PLUGIN_PATH', '').split(os.pathsep):
-            if path == '':
-                continue
-            if not os.path.isabs(path):
-                self.logger.warning(f'"{path}" is not absolute, skipping')
-                continue
-            if not os.path.exists(path):
-                self.logger.warning(f'"{path}" does not exist, skipping')
-                continue
-            extra_paths.append(path)
-            self.logger.debug(f'using extra plugin path: "{path}"')
-
-        plugin_paths.extend((x, 'extra') for x in extra_paths)
 
-        for plugin_path, plugin_category in plugin_paths:
-            for filename in os.listdir(plugin_path):
-                filepath = os.path.join(plugin_path, filename)
-                if os.path.isdir(filepath):
+        self.__loaded_package_paths: List[Path] = []
+        # load all plugins
+        for plugin_path, plugin_category in reversed(plugin_paths):
+            for filepath in plugin_path.iterdir():
+                if filepath.is_dir():
                     self._install_package(filepath, plugin_category)
+                    self.__loaded_package_paths.insert(0, filepath)
                 else:
-                    filebasename, fileext = os.path.splitext(filename)
-                    if fileext != '.py':
+                    if filepath.suffix != '.py':
                         continue
                     self._install_node(filepath, plugin_category)
 
         self.logger.info('loaded node types:\n\t' + '\n\t'.join(self.__plugins.keys()))
         self.logger.info('loaded node presets:\n\t' + '\n\t'.join(f'{pkg}::{label}' for pkg, pkgdata in self.__presets.items() for label in pkgdata.keys()))
 
-        # load default settings
-        default_settings_config_path = paths.config_path('defaults.toml', 'scheduler.nodes')
-        if default_settings_config_path.exists():
-            with open(default_settings_config_path) as f:
-                self.__default_settings_config = toml.load(f)
-
-            bad_defaults = []
-            for node_type, settings_name in self.__default_settings_config.items():
-                if settings_name not in self.__nodes_settings.get(node_type, {}):
-                    self.logger.warning(f'"{settings_name}" is set as default for "{node_type}", but no such settings is loaded')
-                    bad_defaults.append(node_type)
-                    continue
+        # check default settings
+        bad_defaults = []
+        for node_type, settings_name in self.__default_settings_config.items():
+            if settings_name not in self.__nodes_settings.get(node_type, {}):
+                self.logger.warning(f'"{settings_name}" is set as default for "{node_type}", but no such settings is loaded')
+                bad_defaults.append(node_type)
+                continue
 
-    def _install_node(self, filepath, plugin_category, parent_package=None):
+    def _install_node(self, filepath: Path, plugin_category: str, parent_package=None):
         """
 
         :param filepath:
@@ -108,8 +79,7 @@ def _install_node(self, filepath, plugin_category, parent_package=None):
         :param parent_package: path to the base of the package, if this plugin is part of one, else - None
         :return:
         """
-        filename = os.path.basename(filepath)
-        filebasename, fileext = os.path.splitext(filename)
+        filebasename = filepath.stem
 
         # calc module hash
         hasher = hashlib.md5()
@@ -140,7 +110,7 @@ def _install_node(self, filepath, plugin_category, parent_package=None):
         # TODO: what if it's overriding existing module?
         sys.modules[modpath] = mod
 
-    def _install_package(self, package_path, plugin_category):
+    def _install_package(self, package_path: Path, plugin_category: str):
         """
         package structure:
             [package_name:dir]
@@ -155,7 +125,8 @@ def _install_package(self, package_path, plugin_category):
             | |_node1.py    <- these are loaded as usual node plugins
             | |_node2.py    <-/
             |_data          <- just a convenient place to store shit, can be accessed with data from plugin
-            |_settings      <- for future saved nodes settings. not implemented yet
+            |_settings      <- for future saved nodes settings
+            | |_defaults.toml  <- config containing default node settings for node types
             | |_node_type_name1
             | | |_settings1.lbs
             | | |_settings2.lbs
@@ -170,57 +141,55 @@ def _install_package(self, package_path, plugin_category):
         :param plugin_category:
         :return:
         """
-        package_name = os.path.basename(package_path)
+        package_name = package_path.name
         if package_name not in self.__package_locations:  # read logic of this up
-            self.__package_locations[package_name] = Path(package_path)
+            self.__package_locations[package_name] = package_path
         # add extra bin paths
         extra_bins = []
         for subbin in (f'{platform.system().lower()}-{platform.machine().lower()}', 'any'):
-            bin_base_path = os.path.join(package_path, 'bin', subbin)
-            if not os.path.exists(bin_base_path):
+            bin_base_path = package_path / 'bin' / subbin
+            if not bin_base_path.exists():
                 continue
             extra_bins.append(bin_base_path)
         if extra_bins:
-            os.environ['PATH'] = os.pathsep.join(extra_bins) + os.environ['PATH']
+            os.environ['PATH'] = os.pathsep.join(str(x) for x in extra_bins) + os.environ['PATH']  # TODO: this should only be accessible to that one plugin
 
         # install extra python modules
-        python_base_path = os.path.join(package_path, 'python')
-        if os.path.exists(python_base_path):
+        python_base_path = package_path / 'python'
+        if python_base_path.exists():
             sysver = sys.version_info
-            pyvers = [tuple(int(y) for y in x.split('.')) for x in os.listdir(python_base_path) if x.isdigit() or re.match(r'^\d+\.\d+$', x)]
+            pyvers = [tuple(int(y) for y in x.name.split('.')) for x in python_base_path.iterdir() if x.name.isdigit() or re.match(r'^\d+\.\d+$', x.name)]
             pyvers = [x for x in pyvers if x[0] == sysver.major
                                            and (len(x) < 2 or x[1] == sysver.minor)
                                            and (len(x) < 3 or x[2] == sysver.micro)]
             pyvers = sorted(pyvers, key=lambda x: len(x), reverse=True)
             for pyver in pyvers:
-                extra_python = os.path.join(python_base_path, '.'.join(str(x) for x in pyver))
-                sys.path.append(extra_python)
+                extra_python = python_base_path / '.'.join(str(x) for x in pyver)
+                sys.path.append(str(extra_python))
 
-                # TODO: this is questionable, this will affect all child processes, we don't want that
-                os.environ['PYTHONPATH'] = os.pathsep.join((extra_python, os.environ['PYTHONPATH'])) if 'PYTHONPATH' in os.environ else extra_python
+                # TODO: this is questionable, this will affect all child processes, we don't want that, this should only be accessible to that one plugin
+                os.environ['PYTHONPATH'] = os.pathsep.join((str(extra_python), os.environ['PYTHONPATH'])) if 'PYTHONPATH' in os.environ else extra_python
 
         # install nodes
-        nodes_path = os.path.join(package_path, 'nodes')
-        if os.path.exists(nodes_path):
-            for filename in os.listdir(nodes_path):
-                filebasename, fileext = os.path.splitext(filename)
-                if fileext != '.py':
+        nodes_path = package_path / 'nodes'
+        if nodes_path.exists():
+            for filepath in nodes_path.iterdir():
+                if filepath.suffix != '.py':
                     continue
-                self._install_node(os.path.join(nodes_path, filename), plugin_category, package_path)
+                self._install_node(filepath, plugin_category, package_path)
 
         # install presets
-        presets_path = os.path.join(package_path, 'presets')
-        if os.path.exists(presets_path):
-            for filename in os.listdir(presets_path):
-                filebasename, fileext = os.path.splitext(filename)
-                if fileext != '.lbp':
+        presets_path = package_path / 'presets'
+        if presets_path.exists():
+            for filepath in presets_path.iterdir():
+                if filepath.suffix != '.lbp':
                     continue
                 try:
-                    with open(os.path.join(presets_path, filename), 'rb') as f:
+                    with open(filepath, 'rb') as f:
                         snippet = NodeSnippetData.deserialize(f.read())
                     snippet.add_tag('preset')
                 except Exception as e:
-                    self.logger.error(f'failed to load snippet {filebasename}, error: {str(e)}')
+                    self.logger.error(f'failed to load snippet "{filepath.stem}", error: {str(e)}')
                     continue
 
                 if package_name not in self.__presets:
@@ -228,21 +197,28 @@ def _install_package(self, package_path, plugin_category):
                 self.__presets[package_name][snippet.label] = snippet
 
         # install node settings
-        settings_path = os.path.join(package_path, 'settings')
-        if os.path.exists(settings_path):
-            for nodetype_name in os.listdir(settings_path):
-                if nodetype_name not in self.__nodes_settings:
-                    self.__nodes_settings[nodetype_name] = {}
-                nodetype_path = os.path.join(settings_path, nodetype_name)
-                for preset_filename in os.listdir(nodetype_path):
-                    preset_name, fileext = os.path.splitext(preset_filename)
-                    if fileext != '.lbs':
+        settings_path = package_path / 'settings'
+        if settings_path.exists():
+            for nodetype_path in settings_path.iterdir():
+                if not nodetype_path.is_dir():
+                    # ignore files, just look for dirs
+                    continue
+                if nodetype_path.name not in self.__nodes_settings:
+                    self.__nodes_settings[nodetype_path.name] = {}
+                for preset_filepath in nodetype_path.iterdir():
+                    if preset_filepath.suffix != '.lbs':
                         continue
                     try:
-                        with open(os.path.join(nodetype_path, preset_filename), 'r') as f:
-                            self.__nodes_settings[nodetype_name][preset_name] = toml.load(f)
+                        with open(preset_filepath, 'r') as f:
+                            self.__nodes_settings[nodetype_path.name][preset_filepath.stem] = toml.load(f)
                     except Exception as e:
-                        self.logger.error(f'failed to load settings {nodetype_name}/{preset_name}, error: {str(e)}')
+                        self.logger.error(f'failed to load settings {nodetype_path.name}/{preset_filepath.stem}, error: {str(e)}')
+
+            settings_defaults_config_path = settings_path / 'defaults.toml'
+            if settings_defaults_config_path.exists():
+                with open(settings_defaults_config_path) as f:
+                    settings_defaults = toml.load(f)
+                self.__default_settings_config.update(settings_defaults)
 
     def plugin_hash(self, plugin_name) -> str:
         return self.__plugin_file_hashes[plugin_name]
@@ -286,14 +262,20 @@ def node_preset_names(self, package_name: str) -> Set[str]:
     def node_preset(self, package_name: str, preset_name: str) -> NodeSnippetData:
         return self.__presets[package_name][preset_name]
 
-    def add_settings_to_existing_package(self, package_name_or_path: Union[str, Path], node_type_name: str, settings_name: str, settings: Dict[str, Any]):
-
+    def __expand_package_path(self, package_name_or_path) -> Path:
         if isinstance(package_name_or_path, str) and package_name_or_path in self.__package_locations:
             package_name_or_path = self.__package_locations[package_name_or_path]
         else:
             package_name_or_path = Path(package_name_or_path)
         if package_name_or_path not in self.__package_locations.values():
-            raise RuntimeError('no package with that name or pathfound')
+            raise RuntimeError('no package with that name or path found')
+        return package_name_or_path
+
+    def loaded_packages_paths(self) -> Tuple[Path, ...]:
+        return tuple(self.__loaded_package_paths)
+
+    def add_settings_to_existing_package(self, package_name_or_path: Union[str, Path], node_type_name: str, settings_name: str, settings: Dict[str, Any]):
+        package_name_or_path = self.__expand_package_path(package_name_or_path)
 
         # at this point package_name_or_path is path
         assert package_name_or_path.exists()
@@ -302,46 +284,57 @@ def add_settings_to_existing_package(self, package_name_or_path: Union[str, Path
             base_path.mkdir(parents=True, exist_ok=True)
         with open(base_path / (settings_name + '.lbs'), 'w') as f:
             toml.dump(settings, f)
-
+        if package_name_or_path not in self.__loaded_package_paths:
+            return  # if we modified not loaded package - no need to update configuration
         # add to settings
-        self.__nodes_settings.setdefault(node_type_name, {})[settings_name] = settings
+        # TODO: fix, settings taking effect depends on loading order.
+        #  so far it's only used for changing leftmost package, so the problem does not emerge
+        if package_name_or_path == self.__loaded_package_paths[0]:
+            self.__nodes_settings.setdefault(node_type_name, {})[settings_name] = settings
+        else:
+            raise NotImplementedError("TBD")
 
-    def set_settings_as_default(self, node_type_name: str, settings_name: Optional[str]):
+    def set_settings_as_default_in_existing_package(self, package_name_or_path: Union[str, Path], node_type_name: str, settings_name: Optional[str]):
         """
 
+        :param package_name_or_path: existing package path to set defaults in
         :param node_type_name:
         :param settings_name: if None - unset any defaults
         :return:
         """
+        package_name_or_path = self.__expand_package_path(package_name_or_path)
+
         if node_type_name not in self.__nodes_settings:
             raise RuntimeError(f'node type "{self.__nodes_settings}" is unknown')
         if settings_name is not None and settings_name not in self.__nodes_settings[node_type_name]:
             raise RuntimeError(f'node type "{self.__nodes_settings}" doesn\'t have settings "{settings_name}"')
-        if settings_name is None and node_type_name in self.__default_settings_config:
-            self.__default_settings_config.pop(node_type_name)
+        config_path = package_name_or_path / 'settings' / 'defaults.toml'
+        # read existing
+        settings_defaults = {}
+        if config_path.exists():
+            # TODO: refactor this module to work with Package representations
+            #  so that when something is updated in any package - final data can be layered fast
+            with open(config_path, 'r') as f:
+                settings_defaults = toml.load(f)
+        if settings_name is None:
+            settings_defaults.pop(node_type_name)
         else:
-            self.__default_settings_config[node_type_name] = settings_name
-        config_path = paths.config_path('defaults.toml', 'scheduler.nodes')
+            settings_defaults[node_type_name] = settings_name
+        # save config
         config_path.parent.mkdir(parents=True, exist_ok=True)  # ensure it exists
         with open(config_path, 'w') as f:
-            toml.dump(self.__default_settings_config, f)
-
-    # def apply_settings(self, node: BaseNode, settings_name: str) -> None:
-    #     if settings_name not in self.node_settings_names(node.type_name()):
-    #         raise RuntimeError(f'requested settings "{settings_name}" not found for type "{node.type_name()}"')
-    #     settings = self.node_settings(node.type_name(), settings_name)
-    #     node.apply_settings(settings)
-
-    # def create_node(self, type_name: str, name: str) -> BaseNode:
-    #     if type_name not in self.__plugins:
-    #         if type_name == 'basenode':  # debug case! base class should never be created directly!
-    #             self.logger.warning('creating BASENODE. if it\'s not for debug/test purposes - it\'s bad!')
-    #             from .basenode import BaseNode
-    #             node = BaseNode(name)
-    #         raise RuntimeError('unknown plugin')
-    #     node: "BaseNode" = self.__plugins[type_name].node_class()(name)
-    #     # now set defaults, before parent is set to prevent ui callbacks to parent
-    #     if type_name in self.__default_settings_config:
-    #         node.apply_settings(self.__default_settings_config[type_name])
-    #     node.set_data_provider(self)
-    #     return node
+            toml.dump(settings_defaults, f)
+
+        if package_name_or_path not in self.__loaded_package_paths:
+            return  # if we modified not loaded package - no need to update configuration
+
+        # TODO: fix, settings taking effect depends on loading order.
+        #  so far it's only used for changing leftmost package, so the problem does not emerge
+        if package_name_or_path == self.__loaded_package_paths[0]:
+            if settings_name is None:
+                if node_type_name in self.__default_settings_config:
+                    self.__default_settings_config.pop(node_type_name)  # TODO: this is incorrect in case other packages have opinions on that
+            else:
+                self.__default_settings_config[node_type_name] = settings_name
+        else:
+            raise NotImplementedError("TBD")
diff --git a/src/lifeblood/processingcontext.py b/src/lifeblood/processingcontext.py
index c24b9016..313c9bf3 100644
--- a/src/lifeblood/processingcontext.py
+++ b/src/lifeblood/processingcontext.py
@@ -1,11 +1,9 @@
 from types import MappingProxyType
-import re
 
 from .attribute_serialization import deserialize_attributes_core
-from .config import get_config
 from .environment_resolver import EnvironmentResolverArguments
 
-from typing import TYPE_CHECKING, Dict, Optional
+from typing import Dict, Optional, TYPE_CHECKING, Union
 
 if TYPE_CHECKING:
     from .basenode import BaseNode
@@ -44,28 +42,30 @@ def __getattr__(self, item):
             raise AttributeError(f'node has no field {item}')
 
     class ConfigWrapper:
-        def __init__(self, node_type_id):
-            self.__config = get_config('scheduler.nodes')
-            self.__scheduler_globals = dict(get_config('scheduler').get_option_noasync('scheduler.globals', {}))
-            self.__nodetypeid = node_type_id
+        def __init__(self, node_config):
+            self.__node_config = node_config
 
         def get(self, key, default=None):
-            return self.__config.get_option_noasync(f'{self.__nodetypeid}.{key}',
-                                                    self.__scheduler_globals.get(key,
-                                                                                 default))
+            return self.__node_config.get(key, default)
 
         def __getitem__(self, item):
             return self.get(item)
 
-    def __init__(self, node: "BaseNode", task_dict: dict):
+    def __init__(self, node: "BaseNode", task_dict: dict, node_config: Dict[str, Union[str, int, float, list, dict]]):
+        """
+        All information node can access during processing.
+        This is read-only.
+        All modifications are to be done through ProcessingResult
+
+        :param node_config: extra mapping that node can access through parameter expressions
+        """
         task_dict = dict(task_dict)
         self.__task_attributes = deserialize_attributes_core(task_dict.get('attributes', '{}'))
         self.__task_dict = task_dict
         self.__task_wrapper = ProcessingContext.TaskWrapper(task_dict)
         self.__node_wrapper = ProcessingContext.NodeWrapper(node, self)
-        sanitized_name = re.sub(r'\W', lambda m: f'x{ord(m.group(0))}', node.type_name())
         self.__env_args = EnvironmentResolverArguments.deserialize(task_dict.get('environment_resolver_data')) if task_dict.get('environment_resolver_data') is not None else None
-        self.__conf_wrapper = ProcessingContext.ConfigWrapper(sanitized_name)
+        self.__conf_wrapper = ProcessingContext.ConfigWrapper(node_config)
         self.__node = node
 
     def param_value(self, param_name: str):
diff --git a/src/lifeblood/scheduler/data_access.py b/src/lifeblood/scheduler/data_access.py
index 32e79295..9c1e258a 100644
--- a/src/lifeblood/scheduler/data_access.py
+++ b/src/lifeblood/scheduler/data_access.py
@@ -7,17 +7,18 @@
 from ..attribute_serialization import serialize_attributes
 from ..db_misc import sql_init_script
 from ..expiring_collections import ExpiringValuesSetMap
-from ..config import get_config
 from ..enums import TaskState, InvocationState
 from ..worker_metadata import WorkerMetadata
 from ..logging import get_logger
 from ..shared_lazy_sqlite_connection import SharedLazyAiosqliteConnection
 from .. import aiosqlite_overlay
 from ..environment_resolver import EnvironmentResolverArguments
+from ..scheduler_config_provider_base import SchedulerConfigProviderBase
+from ..worker_resource_definition import WorkerResourceDataType
 
-from typing import Any, Dict, Iterable, Optional, Tuple
+from typing import Any, Dict, Iterable, List, Optional, Tuple, Type
 
-SCHEDULER_DB_FORMAT_VERSION = 2
+SCHEDULER_DB_FORMAT_VERSION = 3
 
 
 @dataclass
@@ -41,10 +42,14 @@ class TaskSpawnData:
 
 
 class DataAccess:
-    def __init__(self, db_path, db_connection_timeout):
+    def __init__(
+            self,
+            *,
+            config_provider: SchedulerConfigProviderBase
+    ):
         self.__logger = get_logger('scheduler.data_access')
-        self.db_path: str = db_path
-        self.db_timeout: int = db_connection_timeout
+        self.__db_path: str = config_provider.main_database_location()
+        self.__db_timeout: float = config_provider.main_database_connection_timeout()
 
         # "public" members
         self.mem_cache_workers_resources: dict = {}
@@ -56,14 +61,16 @@ def __init__(self, db_path, db_connection_timeout):
         # on certain submission errors we might want to ban hwid for some time, as it can be assumed
         # that consecutive submission attempts will result in the same error (like package resolution error)
         self.__banned_hwids_per_task: ExpiringValuesSetMap = ExpiringValuesSetMap()
-        self.__ban_time = get_config('scheduler').get_option_noasync('data_access.hwid_ban_timeout', 10)
+        self.__ban_time = config_provider.hardware_ban_timeout()
 
         self.__workers_metadata: Dict[int, WorkerMetadata] = {}
         #
         # ensure database is initialized
-        with sqlite3.connect(db_path) as con:
+        with sqlite3.connect(self.__db_path) as con:
             con.executescript(sql_init_script)
-        with sqlite3.connect(db_path) as con:
+
+        # upgrade db definitions
+        with sqlite3.connect(self.__db_path) as con:
             con.row_factory = sqlite3.Row
             cur = con.execute('SELECT * FROM lifeblood_metadata')
             metadata = cur.fetchone()  # there should be exactly one single row.
@@ -89,6 +96,37 @@ def __init__(self, db_path, db_connection_timeout):
                 cur.close()
             self.__db_uid = struct.unpack('>Q', struct.pack('>q', metadata['unique_db_id']))[0]  # reinterpret signed as unsigned
 
+        # update resource table straight away
+        # for now the logic is to keep existing columns
+        with sqlite3.connect(self.__db_path) as con:
+            con.row_factory = sqlite3.Row
+            cur = con.execute('PRAGMA table_info(resources)')
+            resource_rows = {x['name']: x for x in cur.fetchall() if x['name'] != 'hwid'}
+            cur.close()
+
+            need_commit = False
+            for res_def in config_provider.hardware_resource_definitions():
+                col_type, col_def = {
+                    WorkerResourceDataType.GENERIC_FLOAT: ('INTEGER', float(res_def.default)),  # use INTEGER for floats, as it is more flexible in sqlite, see https://sqlite.org/flextypegood.html
+                    WorkerResourceDataType.GENERIC_INT: ('INTEGER', int(res_def.default)),
+                    WorkerResourceDataType.SHARABLE_COMPUTATIONAL_UNIT: ('INTEGER', int(res_def.default)),
+                    WorkerResourceDataType.MEMORY_BYTES: ('INTEGER', int(res_def.default)),
+                }[res_def.type]
+                if res_def.name in resource_rows:  # skip existing
+                    # dflt_value is string repr of the number, so we have to convert col_def to compare. not the best way for floating numbers
+                    if resource_rows[res_def.name]['type'] != col_type or resource_rows[res_def.name]['dflt_value'] != str(col_def):
+                        self.__logger.warning(f'existing resource definition changed for "{res_def.name}", recreating, all resource data will be lost')
+                        con.execute(f'ALTER TABLE resources DROP COLUMN "{res_def.name}"')
+                        con.execute(f'ALTER TABLE resources DROP COLUMN "total_{res_def.name}"')
+                    else:
+                        continue
+                self.__logger.debug(f'adding new resource "{res_def.name}" of type {col_type} ({col_def}) into the table')
+                con.execute(f'ALTER TABLE resources ADD COLUMN "{res_def.name}" {col_type} NOT NULL DEFAULT {col_def}')
+                con.execute(f'ALTER TABLE resources ADD COLUMN "total_{res_def.name}" {col_type} NOT NULL DEFAULT {col_def}')
+                need_commit = True
+            if need_commit:
+                con.commit()
+
     async def create_node(self, node_type: str, node_name: str, *, con: Optional[aiosqlite.Connection] = None) -> int:
         # TODO: scheduler must use this instead of creating directly
         #  this should be done as part of a bigger refactoring
@@ -363,10 +401,10 @@ def set_worker_metadata(self, worker_hwid, data: WorkerMetadata):
 
     def data_connection(self) -> aiosqlite_overlay.ConnectionWithCallbacks:
         # TODO: con.row_factory = aiosqlite.Row must be here, ALMOST all places use it anyway, need to prune
-        return aiosqlite_overlay.connect(self.db_path, timeout=self.db_timeout, pragmas_after_connect=('synchronous=NORMAL',))
+        return aiosqlite_overlay.connect(self.__db_path, timeout=self.__db_timeout, pragmas_after_connect=('synchronous=NORMAL',))
 
     def lazy_data_transaction(self, key_name: str):
-        return SharedLazyAiosqliteConnection(None, self.db_path, key_name, timeout=self.db_timeout)
+        return SharedLazyAiosqliteConnection(None, self.__db_path, key_name, timeout=self.__db_timeout)
 
     async def write_back_cache(self):
         self.__logger.info('pinger syncing temporary tables back...')
@@ -386,7 +424,7 @@ async def write_back_cache(self):
     def __database_schema_upgrade(self, con: sqlite3.Connection, from_version: int, to_version: int) -> bool:
         if from_version == to_version:
             return False
-        if from_version < 1 or to_version > 2:
+        if from_version < 1 or to_version > 3:
             raise NotImplementedError(f"Don't know how to update db schema from v{from_version} to v{to_version}")
         if to_version < from_version:
             raise ValueError(f'to_version cannot be less than from_version ({to_version}<{from_version})')
@@ -405,3 +443,20 @@ def __database_schema_upgrade(self, con: sqlite3.Connection, from_version: int,
             # need to ensure new node_object_state field is present
             con.execute('ALTER TABLE "nodes" ADD COLUMN "node_object_state" BLOB')
             return True
+        if to_version == 3:
+            # there was a bug in prev init script, wrong column in index, now it randomly pops on rename,
+            # so we need to recreate that index
+            con.executescript(
+                'DROP INDEX task_group_attrs_state_creator_idx;'
+                'CREATE INDEX IF NOT EXISTS "task_group_attrs_state_creator_idx" ON "task_group_attributes" ('
+                '    "state",'
+                '    "creator"'
+                ');'
+            )
+            # need to set default values for existing resource columns
+            for col_name in ('cpu_count', 'total_cpu_count', 'cpu_mem', 'total_cpu_mem', 'gpu_count', 'total_gpu_count', 'gpu_mem', 'total_gpu_mem'):
+                con.execute(f'ALTER TABLE "resources" RENAME COLUMN "{col_name}" TO "__old_{col_name}"')
+                con.execute(f'ALTER TABLE "resources" ADD COLUMN "{col_name}" INTEGER NOT NULL DEFAULT 0')
+                con.execute(f'UPDATE "resources" SET "{col_name}" = "__old_{col_name}"')
+                con.execute(f'ALTER TABLE "resources" DROP COLUMN "__old_{col_name}"')
+            return True
diff --git a/src/lifeblood/scheduler/pinger.py b/src/lifeblood/scheduler/pinger.py
index df02de5e..0016b187 100644
--- a/src/lifeblood/scheduler/pinger.py
+++ b/src/lifeblood/scheduler/pinger.py
@@ -4,9 +4,7 @@
 from .. import logging
 from ..worker_messsage_processor import WorkerControlClient
 from ..enums import WorkerState, InvocationState, WorkerPingState, WorkerPingReply
-from ..ui_protocol_data import TaskDelta
 from .scheduler_component_base import SchedulerComponentBase
-from ..config import get_config
 from ..net_messages.address import AddressChain
 from ..net_messages.exceptions import MessageTransferError, MessageTransferTimeoutError
 
@@ -17,15 +15,14 @@
 
 
 class Pinger(SchedulerComponentBase):
-    def __init__(self, scheduler: "Scheduler"):
+    def __init__(
+            self,
+            scheduler: "Scheduler",
+    ):
         super().__init__(scheduler)
         self.__pinger_logger = logging.get_logger('scheduler.worker_pinger')
-        config = get_config('scheduler')
 
-        self.__ping_interval = config.get_option_noasync('scheduler.pinger.ping_interval', 10)  # interval for active workers (workers doing work)
-        self.__ping_idle_interval = config.get_option_noasync('scheduler.pinger.ping_idle_interval', 30)  # interval for idle workers
-        self.__ping_off_interval = config.get_option_noasync('scheduler.pinger.ping_off_interval', 60)  # interval for off/errored workers  (not really used since workers need to report back first)
-        self.__dormant_mode_ping_interval_multiplier = config.get_option_noasync('scheduler.pinger.dormant_ping_multiplier', 5)
+        self.__ping_interval, self.__ping_idle_interval, self.__ping_off_interval, self.__dormant_mode_ping_interval_multiplier = self.scheduler.config_provider.ping_intervals()
         self.__ping_interval_mult = 1
 
     def _main_task(self):
diff --git a/src/lifeblood/scheduler/scheduler.py b/src/lifeblood/scheduler/scheduler.py
index 40defef4..c924780e 100644
--- a/src/lifeblood/scheduler/scheduler.py
+++ b/src/lifeblood/scheduler/scheduler.py
@@ -1,4 +1,3 @@
-import sys
 import os
 from pathlib import Path
 import time
@@ -12,18 +11,17 @@
 from contextlib import asynccontextmanager
 
 from .. import logging
-from .. import paths
 from ..nodegraph_holder_base import NodeGraphHolderBase
 from ..attribute_serialization import serialize_attributes, deserialize_attributes
 from ..worker_messsage_processor import WorkerControlClient
 from ..scheduler_task_protocol import SchedulerTaskProtocol, SpawnStatus
 from ..scheduler_ui_protocol import SchedulerUiProtocol
-from ..invocationjob import InvocationJob
+from ..hardware_resources import HardwareResources
+from ..invocationjob import InvocationJob, Requirements
 from ..environment_resolver import EnvironmentResolverArguments
 from ..broadcasting import create_broadcaster
 from ..simple_worker_pool import WorkerPool
 from ..nethelpers import get_broadcast_addr_for, all_interfaces
-from ..net_classes import WorkerResources
 from ..worker_metadata import WorkerMetadata
 from ..taskspawn import TaskSpawn
 from ..basenode import BaseNode
@@ -32,13 +30,12 @@
 from ..basenode_serialization import NodeSerializerBase, IncompatibleDeserializationMethod, FailedToDeserialize
 from ..enums import WorkerState, WorkerPingState, TaskState, InvocationState, WorkerType, \
     SchedulerMode, TaskGroupArchivedState
-from ..config import get_config
-from ..defaults import scheduler_port as default_scheduler_port, ui_port as default_ui_port, scheduler_message_port as default_scheduler_message_port
 from .. import aiosqlite_overlay
 from ..ui_protocol_data import TaskData, TaskDelta, IncompleteInvocationLogData, InvocationLogData
 
 from ..net_messages.address import DirectAddress, AddressChain
 from ..scheduler_message_processor import SchedulerMessageProcessor
+from ..scheduler_config_provider_base import SchedulerConfigProviderBase
 
 from .data_access import DataAccess
 from .scheduler_component_base import SchedulerComponentBase
@@ -50,22 +47,15 @@
 
 
 class Scheduler(NodeGraphHolderBase):
-    def __init__(self, db_file_path, *,
+    def __init__(self, *,
+                 scheduler_config_provider: SchedulerConfigProviderBase,
                  node_data_provider: NodeDataProvider,
                  node_serializers: List[NodeSerializerBase],
-                 do_broadcasting: Optional[bool] = None,
-                 broadcast_interval: Optional[int] = None,
-                 helpers_minimal_idle_to_ensure=1,
-                 server_addr: Optional[Tuple[str, int, int]] = None,
-                 server_ui_addr: Optional[Tuple[str, int]] = None):
+                 ):
         """
         TODO: add a docstring
 
-        :param db_file_path:
-        :param do_broadcasting:
-        :param helpers_minimal_idle_to_ensure:
-        :param server_addr:
-        :param server_ui_addr:
+        :param scheduler_config_provider:
         """
         self.__node_data_provider: NodeDataProvider = node_data_provider
         if len(node_serializers) < 1:
@@ -76,8 +66,7 @@ def __init__(self, db_file_path, *,
         self.__node_objects: Dict[int, BaseNode] = {}
         self.__node_objects_locks: Dict[int, RWLock] = {}
         self.__node_objects_creation_locks: Dict[int, asyncio.Lock] = {}
-        config = get_config('scheduler')
-        self.__config = config
+        self.__config_provider: SchedulerConfigProviderBase = scheduler_config_provider
 
         # this lock will prevent tasks from being reported cancelled and done at the same exact time should that ever happen
         # this lock is overkill already, but we can make it even more overkill by using set of locks for each invoc id
@@ -86,22 +75,18 @@ def __init__(self, db_file_path, *,
 
         self.__all_components = None
         self.__started_event = asyncio.Event()
-
-        if db_file_path is None:
-            db_file_path = config.get_option_noasync('core.database.path', str(paths.default_main_database_location()))
-        if not db_file_path.startswith('file:'):  # if schema is used - we do not modify the db uri in any way
-            db_file_path = os.path.realpath(os.path.expanduser(db_file_path))
-
-        self.__logger.debug(f'starting scheduler with database: {db_file_path}')
-
-        self.db_path = db_file_path
-        self.data_access: DataAccess = DataAccess(db_file_path, 30)
+        
+        self.__db_path = scheduler_config_provider.main_database_location()
+        if not self.__db_path.startswith('file:'):  # if schema is used - we do not modify the db uri in any way
+            self.__db_path = os.path.realpath(os.path.expanduser(self.__db_path))
+        self.__logger.debug(f'starting scheduler with database: {self.__db_path}')
+        self.data_access: DataAccess = DataAccess(
+            config_provider=self.__config_provider,
+        )
         ##
 
-        self.__use_external_log = config.get_option_noasync('core.database.store_logs_externally', False)
-        self.__external_log_location: Optional[Path] = config.get_option_noasync('core.database.store_logs_externally_location', None)
-        if self.__use_external_log and not self.__external_log_location:
-            raise SchedulerConfigurationError('if store_logs_externally is set - store_logs_externally_location must be set too')
+        self.__use_external_log = self.__config_provider.external_log_location() is not None
+        self.__external_log_location: Optional[Path] = self.__config_provider.external_log_location()
         if self.__use_external_log:
             external_log_path = Path(self.__use_external_log)
             if external_log_path.exists() and external_log_path.is_file():
@@ -119,33 +104,12 @@ def __init__(self, db_file_path, *,
         self.__ui_address = None
         self.__legacy_command_server_address = None
 
-        if server_addr is not None:
-            server_ip, legacy_server_port, message_server_port = server_addr
-        else:
-            legacy_server_port = config.get_option_noasync(
-                'core.legacy_server_port',
-                config.get_option_noasync(
-                    'core.server_port',
-                    default_scheduler_port()
-                )
-            )
-            message_server_port = config.get_option_noasync('core.server_message_port', default_scheduler_message_port())
-            server_ip = config.get_option_noasync('core.server_ip', '0.0.0.0')  # use all ifaces by default
-            
-        if server_ip == '0.0.0.0':  # message_processor address must be addressable, no catchall
-            for iface_ip in all_interfaces():
-                self.__message_processor_addresses.append(DirectAddress.from_host_port(iface_ip, message_server_port))
-        else:
-            self.__message_processor_addresses.append(DirectAddress.from_host_port(server_ip, message_server_port))
-        self.__legacy_command_server_address = (server_ip, legacy_server_port)
-            
-        if server_ui_addr is None:
-            self.__ui_address = (
-                config.get_option_noasync('core.ui_ip', server_ip or '0.0.0.0'),
-                config.get_option_noasync('core.ui_port', default_ui_port())
-            )
-        else:
-            self.__ui_address = server_ui_addr
+        legacy_server_ip, legacy_server_port = self.__config_provider.legacy_server_address()  # TODO: this CAN be None
+        for message_server_ip, message_server_port in self.__config_provider.server_message_addresses():
+            self.__message_processor_addresses.append(DirectAddress.from_host_port(message_server_ip, message_server_port))
+        self.__legacy_command_server_address = (legacy_server_ip, legacy_server_port)
+
+        self.__ui_address = self.__config_provider.server_ui_address()
 
         self.__stop_event = asyncio.Event()
         self.__server_closing_task = None
@@ -155,22 +119,22 @@ def __init__(self, db_file_path, *,
         self.__message_processor: Optional[SchedulerMessageProcessor] = None
         self.__ui_server = None
         self.__ui_server_coro_args = {'protocol_factory': self._ui_protocol_factory, 'host': self.__ui_address[0], 'port': self.__ui_address[1], 'backlog': 16}
-        self.__legacy_server_coro_args = {'protocol_factory': self._scheduler_protocol_factory, 'host': server_ip, 'port': legacy_server_port, 'backlog': 16}
-
-        if do_broadcasting is None:
-            do_broadcasting = config.get_option_noasync('core.broadcast', True)
-        if broadcast_interval is None or broadcast_interval <= 0:
-            broadcast_interval = config.get_option_noasync('core.broadcast_interval', 10)
-        self.__do_broadcasting = do_broadcasting
-        self.__broadcasting_interval = broadcast_interval
+        self.__legacy_server_coro_args = {'protocol_factory': self._scheduler_protocol_factory, 'host': legacy_server_ip, 'port': legacy_server_port, 'backlog': 16}
+
+        self.__do_broadcasting = self.__config_provider.broadcast_interval() is not None
+        self.__broadcasting_interval = self.__config_provider.broadcast_interval() or 0
         self.__broadcasting_servers = []
 
         self.__worker_pool = None
-        self.__worker_pool_helpers_minimal_idle_to_ensure = helpers_minimal_idle_to_ensure
+        self.__worker_pool_helpers_minimal_idle_to_ensure = self.__config_provider.scheduler_helpers_minimal()
 
         self.__event_loop = asyncio.get_running_loop()
         assert self.__event_loop is not None, 'Scheduler MUST be created within working event loop, in the main thread'
 
+    @property
+    def config_provider(self) -> SchedulerConfigProviderBase:
+        return self.__config_provider
+
     def get_event_loop(self):
         return self.__event_loop
 
@@ -276,17 +240,18 @@ async def _get_node_object_by_id(self, node_id: int) -> BaseNode:
                     try:
                         for serializer in self.__node_serializers:
                             try:
-                                node_object = await serializer.deserialize_async(self, node_id, self.__node_data_provider, node_row['node_object'], node_row['node_object_state'])
+                                node_object = await serializer.deserialize_async(self.__node_data_provider, node_row['node_object'], node_row['node_object_state'])
                                 break
                             except IncompatibleDeserializationMethod as e:
                                 self.__logger.warning(f'deserialization method failed with {e} ({serializer})')
                                 continue
                         else:
                             raise FailedToDeserialize(f'node entry {node_id} has unknown serialization method')
+                        node_object.set_parent(self, node_id)
                         self.__node_objects[node_id] = node_object
                         return self.__node_objects[node_id]
                     except FailedToDeserialize:
-                        if self.__config.get_option_noasync('core.ignore_node_deserialization_failures', False):
+                        if self.__config_provider.ignore_node_deserialization_failures():
                             pass  # ignore errors, recreate node
                         else:
                             raise
@@ -803,7 +768,9 @@ async def __task_cancel_reported_inner(self, task: InvocationJob, stdout: str, s
     #
     # add new worker to db
     async def add_worker(
-            self, addr: str, worker_type: WorkerType, worker_resources: WorkerResources,  # TODO: all resource should also go here
+            # TODO: WorkerResources (de)serialization
+            # TODO: Worker actually passing new WorkerResources on hello
+            self, addr: str, worker_type: WorkerType, worker_resources: HardwareResources,  # TODO: all resource should also go here
             *,
             assume_active: bool = True,
             worker_metadata: WorkerMetadata):
@@ -877,27 +844,28 @@ async def add_worker(
                 #                   '(?, ?, ?)',
                 #                   (worker_id, tstamp, ping_state))
 
+            resource_fields = tuple(x.name for x in self.__config_provider.hardware_resource_definitions())
+            # checks
+            for field in resource_fields:
+                if field not in worker_resources:
+                    self.__logger.warning(f'worker (hwid:{worker_resources.hwid}) does not declare expected resource "{field}", assume value=0')
+            for res_name, _ in worker_resources.items():
+                if res_name not in resource_fields:
+                    self.__logger.warning(f'worker (hwid:{worker_resources.hwid}) declares resource "{res_name}" unknown to the scheduler')
+
             await con.execute('INSERT INTO resources '
-                              '(hwid, cpu_count, total_cpu_count, '
-                              'cpu_mem, total_cpu_mem, '
-                              'gpu_count, total_gpu_count, '
-                              'gpu_mem, total_gpu_mem) '
-                              'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) '
-                              'ON CONFLICT(hwid) DO UPDATE SET '
-                              'cpu_count=excluded.cpu_count, total_cpu_count=excluded.total_cpu_count, '
-                              'cpu_mem=excluded.cpu_mem, total_cpu_mem=excluded.total_cpu_mem, '
-                              'gpu_count=excluded.gpu_count, total_gpu_count=excluded.total_gpu_count, '
-                              'gpu_mem=excluded.gpu_mem, total_gpu_mem=excluded.total_gpu_mem',
+                              '(hwid, ' +
+                              ', '.join(f'{field}, total_{field}' for field in resource_fields) +
+                              ') '
+                              'VALUES (?' + ', ?'*(2*len(resource_fields)) + ') '
+                              'ON CONFLICT(hwid) DO UPDATE SET ' +
+                              ', '.join(f'"{field}"=excluded.{field}, "total_{field}"=excluded.total_{field}' for field in resource_fields)
+                              ,
                               (worker_resources.hwid,
-                               worker_resources.cpu_count,
-                               worker_resources.total_cpu_count,
-                               worker_resources.cpu_mem,
-                               worker_resources.total_cpu_mem,
-                               worker_resources.gpu_count,
-                               worker_resources.total_gpu_count,
-                               worker_resources.gpu_mem,
-                               worker_resources.total_gpu_mem)
-                               )
+                               *(x for field in resource_fields for x in (
+                                   (worker_resources[field].value, worker_resources[field].value) if field in worker_resources else (0, 0))
+                                 ))
+                              )
             await self._update_worker_resouce_usage(worker_id, hwid=worker_resources.hwid, connection=con)  # used resources are inited to none
             self.data_access.set_worker_metadata(worker_resources.hwid, worker_metadata)
             await con.commit()
@@ -909,7 +877,7 @@ async def add_worker(
     #  or when scheduler picked worker and about to run this, which will lead to inconsistency warning
     #  NOTE!: so far it's always called from a STARTED transaction, so there should not be reentry possible
     #  But that is not enforced right now, easy to make mistake
-    async def _update_worker_resouce_usage(self, worker_id: int, resources: Optional[dict] = None, *, hwid=None, connection: aiosqlite.Connection) -> bool:
+    async def _update_worker_resouce_usage(self, worker_id: int, resources: Optional[Requirements] = None, *, hwid=None, connection: aiosqlite.Connection) -> bool:
         """
         updates resource information based on new worker resources usage
         as part of ongoing transaction
@@ -921,7 +889,8 @@ async def _update_worker_resouce_usage(self, worker_id: int, resources: Optional
         :return: if commit is needed on connection (if db set operation happened)
         """
         assert connection.in_transaction, 'expectation failure'
-        resource_fields = ('cpu_count', 'cpu_mem', 'gpu_count', 'gpu_mem')
+        resource_definitions = self.__config_provider.hardware_resource_definitions()
+        resource_fields = tuple(x.name for x in resource_definitions)
 
         workers_resources = self.data_access.mem_cache_workers_resources
         if hwid is None:
@@ -954,15 +923,14 @@ async def _update_worker_resouce_usage(self, worker_id: int, resources: Optional
         else:
             workers_resources[worker_id] = {}
             for field in resource_fields:
-                if field not in resources:
+                if field not in resources.resources:
                     continue
-                if available_res[field] < resources[field]:
-                    raise NotEnoughResources(f'{field}: {resources[field]} out of {available_res[field]}')
+                if available_res[field] < resources.resources[field].min:
+                    raise NotEnoughResources(f'{field}: {resources.resources[field].min} out of {available_res[field]}')
                 # so we take preferred amount of resources (or minimum if pref not set), but no more than available
                 # if preferred is lower than min - it's ignored
                 workers_resources[worker_id][field] = min(available_res[field],
-                                                          max(resources.get(f'pref_{field}', resources[field]),
-                                                              resources[field]))
+                                                          max(resources.resources[field].pref, resources.resources[field].min))
                 available_res[field] -= workers_resources[worker_id][field]
 
             workers_resources[worker_id]['hwid'] = hwid  # just to ensure it was not overriden
diff --git a/src/lifeblood/scheduler/task_processor.py b/src/lifeblood/scheduler/task_processor.py
index eaaec5b5..9d36f302 100644
--- a/src/lifeblood/scheduler/task_processor.py
+++ b/src/lifeblood/scheduler/task_processor.py
@@ -11,13 +11,12 @@
 from ..enums import WorkerState, InvocationState, TaskState, TaskGroupArchivedState, TaskScheduleStatus
 from ..misc import atimeit
 from ..worker_messsage_processor import WorkerControlClient
-from ..invocationjob import InvocationJob
+from ..invocationjob import InvocationJob, Requirements, InvocationRequirements
 from ..environment_resolver import EnvironmentResolverArguments
 from ..nodethings import ProcessingResult
 from ..attribute_serialization import serialize_attributes, deserialize_attributes
 from ..exceptions import *
 from .. import aiosqlite_overlay
-from ..config import get_config
 from ..ui_events import TaskData, TaskDelta
 from ..net_messages.address import AddressChain
 
@@ -33,7 +32,10 @@
 # tracemalloc.start()
 
 class TaskProcessor(SchedulerComponentBase):
-    def __init__(self, scheduler: "Scheduler"):
+    def __init__(
+            self,
+            scheduler: "Scheduler",
+    ):
         super().__init__(scheduler)
         self.__logger = logging.get_logger('scheduler.task_processor')
 
@@ -45,8 +47,8 @@ def __init__(self, scheduler: "Scheduler"):
         # task processing coroutimes
         self.awaiter_executor = ThreadPoolExecutor(thread_name_prefix='awaiter')  # TODO: max_workers= set from config
 
-        config = get_config('scheduler')
-        self.__invocation_attempts = config.get_option_noasync('invocation.default_attempts', 3)  # TODO: config should be directly used when needed to allow dynamically reconfigure running scheduler
+        self.__invocation_attempts = self.scheduler.config_provider.invocation_attempts()
+        self.__housekeeping_interval = self.scheduler.config_provider.task_processor_housekeeping_interval()
 
     def _main_task(self):
         return self.task_processor()
@@ -61,7 +63,7 @@ def _my_sleep(self):
         self.__processing_interval_mult = self.__dormant_mode_processing_interval_multiplier
 
     @atimeit()
-    async def _awaiter(self, processor_to_run, task_row, abort_state: TaskState, skip_state: TaskState):  # TODO: process task generation errors
+    async def _awaiter(self, processor_to_run, task_row, node_config, abort_state: TaskState, skip_state: TaskState):  # TODO: process task generation errors
         _bench_point_0 = time.perf_counter()
         task_id = task_row['id']
         loop = asyncio.get_event_loop()
@@ -69,7 +71,7 @@ async def _awaiter(self, processor_to_run, task_row, abort_state: TaskState, ski
         try:
             async with self.scheduler.get_node_lock_by_id(task_row['node_id']).reader_lock:
                 time_processing_start = time.perf_counter()
-                process_result: ProcessingResult = await loop.run_in_executor(self.awaiter_executor, processor_to_run, task_row)  # TODO: this should have task and node attributes!
+                process_result: ProcessingResult = await loop.run_in_executor(self.awaiter_executor, processor_to_run, task_row, node_config)  # TODO: this should have task and node attributes!
                 self.__logger.debug(f'(post)processing for t{task_id}:n{task_row["node_id"]} took {time.perf_counter()-time_processing_start:.4f}')
         except NodeNotReadyToProcess as e:
             async with self.awaiter_lock, self.scheduler.data_access.lazy_data_transaction('awaiter_con') as con:
@@ -156,8 +158,7 @@ async def _awaiter(self, processor_to_run, task_row, abort_state: TaskState, ski
                         process_result.invocation_job._set_envresolver_arguments(await EnvironmentResolverArguments.deserialize_async(task_row['environment_resolver_data']))
 
                     taskdada_serialized = await process_result.invocation_job.serialize_async()
-                    invoc_requirements_sql = process_result.invocation_job.requirements().final_where_clause()
-                    invoc_requirements_dict_str = json.dumps(process_result.invocation_job.requirements().to_dict(resources_only=True))
+                    invoc_requirements_stash = process_result.invocation_job.requirements().pack_selection_info()
                     job_priority = process_result.invocation_job.priority()
                     async with con.execute('SELECT MAX(task_group_attributes.priority) AS priority FROM task_group_attributes '
                                            'INNER JOIN task_groups ON task_group_attributes."group"==task_groups."group" '
@@ -170,7 +171,7 @@ async def _awaiter(self, processor_to_run, task_row, abort_state: TaskState, ski
                     await con.execute('UPDATE tasks SET "work_data" = ?, "work_data_invocation_attempt" = 0, "state" = ?, "_invoc_requirement_clause" = ?, '
                                       'priority = ? '
                                       'WHERE "id" = ?',
-                                      (taskdada_serialized, TaskState.READY.value, ':::'.join((invoc_requirements_sql, invoc_requirements_dict_str)),
+                                      (taskdada_serialized, TaskState.READY.value,  invoc_requirements_stash,
                                        group_priority + job_priority,
                                        task_id))
                     ui_task_delta.work_data_invocation_attempt = 0  # for ui event
@@ -492,7 +493,6 @@ async def task_processor(self):
         kick_wait_task = asyncio.create_task(self._poke_event.wait())
         gc_counter = 0
         housekeeping_timestamp = time.monotonic()
-        housekeeping_interval = await get_config('scheduler').get_option('task_processor.housekeeping_interval', 60)
         # tm_counter = 0
         self._main_task_is_ready_now()
         while not self._stop_event.is_set():
@@ -543,7 +543,7 @@ async def task_processor(self):
 
             # housekeeping
             housekeeping_now = time.monotonic()
-            if housekeeping_now - housekeeping_timestamp > housekeeping_interval:
+            if housekeeping_now - housekeeping_timestamp > self.__housekeeping_interval:
                 await self.__task_processor_housekeeping()
                 housekeeping_timestamp = housekeeping_now
 
@@ -558,7 +558,7 @@ async def task_processor(self):
                     _debug_sel = time.perf_counter()
                     async with con.execute('SELECT tasks.id, tasks.parent_id, tasks.children_count, tasks.active_children_count, tasks.state, {attrs}'
                                            'tasks.node_id, tasks.node_input_name, tasks.node_output_name, tasks.name, tasks.split_level, '
-                                           'tasks.work_data, tasks.work_data_invocation_attempt, tasks._invoc_requirement_clause, '
+                                           '{maybe_get_req_clause}'
                                            'nodes.type as node_type, nodes.id as node_id, '
                                            'task_splits.split_id as split_id, task_splits.split_element as split_element, task_splits.split_count as split_count, task_splits.origin_task_id as split_origin_task_id '
                                            'FROM tasks INNER JOIN nodes ON tasks.node_id=nodes.id '
@@ -568,7 +568,8 @@ async def task_processor(self):
                                            'AND dead = 0 '
                                            'ORDER BY {prio_sort} RANDOM()'.format(
                             prio_sort='tasks.priority DESC, ' if task_state == TaskState.READY else '',
-                            attrs='attributes, environment_resolver_data, ' if task_state in (TaskState.WAITING, TaskState.POST_WAITING) else ''
+                            attrs='attributes, environment_resolver_data, ' if task_state in (TaskState.WAITING, TaskState.POST_WAITING) else '',
+                            maybe_get_req_clause='tasks.work_data, tasks.work_data_invocation_attempt, tasks._invoc_requirement_clause, ' if task_state == TaskState.READY else '',
                             ),
 
                                            (task_state.value,)) as cur:
@@ -613,7 +614,8 @@ async def task_processor(self):
                                     self.__logger.exception('unexpected exception in deserialization, failing affected tasks')
                                     set_to_stuff.append((TaskState.ERROR.value, task_row['id']))
                                     continue
-                                awaiters.append(self._awaiter(node_object._process_task_wrapper, dict(task_row),
+                                node_config = self.scheduler.config_provider.node_configuration(node_object.type_name())
+                                awaiters.append(self._awaiter(node_object._process_task_wrapper, dict(task_row), node_config,
                                                               abort_state=TaskState.WAITING, skip_state=TaskState.POST_WAITING))
                         if set_to_stuff:
                             # ui event
@@ -653,7 +655,8 @@ async def task_processor(self):
                                     self.__logger.exception('unexpected exception in deserialization, failing affected tasks')
                                     set_to_stuff.append((TaskState.ERROR.value, task_row['id']))
                                     continue
-                                awaiters.append(self._awaiter(node_object._postprocess_task_wrapper, dict(task_row),
+                                node_config = self.scheduler.config_provider.node_configuration(node_object.type_name())
+                                awaiters.append(self._awaiter(node_object._postprocess_task_wrapper, dict(task_row), node_config,
                                                               abort_state=TaskState.POST_WAITING, skip_state=TaskState.DONE))
                         if set_to_stuff:
                             # ui event
@@ -800,11 +803,8 @@ async def __process_ready(self, all_task_rows, tasks_to_wait, *, con: aiosqlite_
                 self.__logger.warning(f'{task_row["id"]} reached maximum invocation attempts, setting it to error state')
                 continue
             #
-            requirements_clause_sql: str = task_row["_invoc_requirement_clause"]
-            requirements_clause_dict = None
-            if (splitpos := requirements_clause_sql.rfind(':::')) > -1:
-                requirements_clause_dict = json.loads(requirements_clause_sql[splitpos + 3:])
-                requirements_clause_sql = requirements_clause_sql[:splitpos]
+            requirements_clause_sql, requirements_clause = InvocationRequirements.unpack_selection_info(task_row["_invoc_requirement_clause"])
+
             if requirements_clause_sql in where_empty_cache:
                 continue
             try:
@@ -856,7 +856,7 @@ async def __process_ready(self, all_task_rows, tasks_to_wait, *, con: aiosqlite_
 
             # set resource usage straight away
             try:
-                await self.scheduler._update_worker_resouce_usage(worker['id'], resources=requirements_clause_dict, hwid=worker['hwid'], connection=con)
+                await self.scheduler._update_worker_resouce_usage(worker['id'], resources=requirements_clause, hwid=worker['hwid'], connection=con)
             except NotEnoughResources:
                 self.__logger.error(f'inconsistency in worker resource tracking! could not submit to worker {worker["id"]}')
                 continue
diff --git a/src/lifeblood/scheduler/ui_state_accessor.py b/src/lifeblood/scheduler/ui_state_accessor.py
index 086a0c01..3341cd08 100644
--- a/src/lifeblood/scheduler/ui_state_accessor.py
+++ b/src/lifeblood/scheduler/ui_state_accessor.py
@@ -1,13 +1,9 @@
 import asyncio
-import dataclasses
-
 import aiosqlite
 from concurrent.futures import ThreadPoolExecutor
-from datetime import datetime
 from dataclasses import dataclass
 import time
 from enum import Enum
-from queue import Queue
 from ..logging import get_logger
 from ..misc import atimeit, aperformance_measurer
 from ..enums import InvocationState, TaskState, TaskGroupArchivedState, WorkerState, WorkerType, UIEventType
@@ -15,9 +11,9 @@
 from ..scheduler_event_log import SchedulerEventLog
 from ..ui_events import TaskEvent, TaskFullState, TasksUpdated, TasksRemoved, TasksChanged
 from ..ui_protocol_data import TaskBatchData, UiData, TaskGroupData, TaskGroupBatchData, TaskGroupStatisticsData, \
-    NodeGraphStructureData, WorkerBatchData, WorkerData, WorkerResources, NodeConnectionData, NodeData, TaskData, TaskDelta
+    NodeGraphStructureData, WorkerBatchData, WorkerData, WorkerResource, WorkerResourceType, WorkerResources, NodeConnectionData, NodeData, TaskData, TaskDelta
 from .scheduler_component_base import SchedulerComponentBase
-from .data_access import DataAccess
+from ..worker_resource_definition import WorkerResourceDefinition, WorkerResourceDataType
 
 from typing import Dict, Iterable, List, Optional, Tuple, TYPE_CHECKING, Set, Union
 
@@ -420,18 +416,12 @@ async def get_nodes_ui_state(self) -> NodeGraphStructureData:
 
     async def get_workers_ui_state(self) -> WorkerBatchData:
         self.__logger.debug('workers update')
+        resource_definitions = self.scheduler.config_provider.hardware_resource_definitions()
         async with self.__data_access.data_connection() as con, \
                 aperformance_measurer(threshold_to_report=0.005, name='get_workers_ui_state'):
             con.row_factory = aiosqlite.Row
-            async with con.execute('SELECT workers."id", '
-                                   'cpu_count, '
-                                   'total_cpu_count, '
-                                   'cpu_mem, '
-                                   'total_cpu_mem, '
-                                   'gpu_count, '
-                                   'total_gpu_count, '
-                                   'gpu_mem, '
-                                   'total_gpu_mem, '
+            async with con.execute('SELECT workers."id", ' +
+                                   ''.join(f'{rd.name}, total_{rd.name}, ' for rd in resource_definitions) +
                                    'workers."hwid", '
                                    'last_address, workers."state", worker_type, invocations.node_id, invocations.task_id, invocations."id" as invoc_id, '
                                    'GROUP_CONCAT(worker_groups."group") as groups '
@@ -448,7 +438,7 @@ async def get_workers_ui_state(self) -> WorkerBatchData:
                 for worker_data in all_workers.values():
                     worker_data['groups'] = set(worker_data['groups'].split(',')) if worker_data['groups'] else set()
 
-        return await asyncio.get_event_loop().run_in_executor(None, _pack_workers_from_raw, self.__data_access.db_uid, all_workers)
+        return await asyncio.get_event_loop().run_in_executor(None, _pack_workers_from_raw, self.__data_access.db_uid, all_workers, resource_definitions)
 
     #
     # task group mapping related crap
@@ -641,17 +631,32 @@ def subscriptions_count(self):
 
 # scheduler helpers
 
-def _pack_workers_from_raw(db_uid: int, ui_workers: dict) -> "WorkerBatchData":
+
+def _pack_workers_from_raw(db_uid: int, ui_workers: dict, resource_definitions: Tuple[WorkerResourceDefinition, ...]) -> "WorkerBatchData":
     """
     this is scheduler helper function, it's incoming data format is dictated purely by scheduler
     """
     workers = {}
     for worker_id, worker_raw in ui_workers.items():
         assert worker_id == worker_raw['id']
-        res = WorkerResources(worker_raw['cpu_count'], worker_raw['total_cpu_count'],
-                              worker_raw['cpu_mem'], worker_raw['total_cpu_mem'],
-                              worker_raw['gpu_count'], worker_raw['total_gpu_count'],
-                              worker_raw['gpu_mem'], worker_raw['total_gpu_mem'])
+        ress = []
+        for res_def in resource_definitions:
+            if res_def.type in (WorkerResourceDataType.GENERIC_INT, WorkerResourceDataType.MEMORY_BYTES):
+                res_type = WorkerResourceType.INT
+                convert_to_data_type = int
+            elif res_def.type in (WorkerResourceDataType.GENERIC_FLOAT, WorkerResourceDataType.SHARABLE_COMPUTATIONAL_UNIT):
+                res_type = WorkerResourceType.FLOAT
+                convert_to_data_type = float
+            else:
+                raise NotImplementedError(f'unhandled worker resource definition type: {res_def.type}')
+            ress.append(WorkerResource(
+                convert_to_data_type(worker_raw[res_def.name]),
+                convert_to_data_type(worker_raw[f'total_{res_def.name}']),
+                res_type,
+                res_def.name,
+            ))
+
+        res = WorkerResources(ress)
         workers[worker_id] = WorkerData(worker_id, res, str(worker_raw['hwid']), worker_raw['last_address'], worker_raw['last_seen'],
                                         WorkerState(worker_raw['state']), WorkerType(worker_raw['worker_type']),
                                         worker_raw['node_id'], worker_raw['task_id'], worker_raw['invoc_id'], worker_raw['progress'],
@@ -714,13 +719,3 @@ def _pack_task_groups(db_uid: int, all_task_groups) -> "TaskGroupBatchData":
                                                 group_raw['priority'], stat)
 
     return TaskGroupBatchData(db_uid, task_groups)
-
-
-def _create_uidata_from_raw_noasync(db_uid, ui_nodes, ui_connections, ui_tasks, ui_workers, all_task_groups):
-
-    node_graph_data = _pack_nodes_connections_data(db_uid, ui_nodes, ui_connections)
-    tasks = _pack_tasks_data(db_uid, ui_tasks)
-    worker_batch_data = _pack_workers_from_raw(db_uid, ui_workers)
-    task_groups = _pack_task_groups(db_uid, all_task_groups)
-
-    return UiData(db_uid, node_graph_data, tasks, worker_batch_data, task_groups)
diff --git a/src/lifeblood/scheduler_config_provider_base.py b/src/lifeblood/scheduler_config_provider_base.py
new file mode 100644
index 00000000..cc0f22e0
--- /dev/null
+++ b/src/lifeblood/scheduler_config_provider_base.py
@@ -0,0 +1,138 @@
+from .worker_resource_definition import WorkerResourceDefinition
+from pathlib import Path
+
+from typing import Optional, Tuple
+
+
+class SchedulerConfigProviderBase:
+    """
+    Scheduler Config Provider interface
+
+    this should be responsible for using config values and making sense of them
+    nothing in scheduler should use get_config directly and parse it's values by itself
+
+    Currently, Scheduler Config Providers are supposed to be IMMUTABLE,
+     meaning any config changes at runtime are NOT supported, as there are no mechanisms of informing interested parties (config users)
+     about the changes
+    """
+    def main_database_location(self) -> str:
+        """
+        location of the main task database.
+        probably file path, but can be something else if more things are supported
+        """
+        raise NotImplementedError()
+
+    def main_database_connection_timeout(self) -> float:
+        """
+        database connection timeout
+        """
+        raise NotImplementedError()
+
+    def node_configuration(self, node_type_id: str) -> dict:
+        """
+        arbitrary key-value pairs as defined by the scheduler's node configuration
+        some nodes may have configuration stored in scheduler-side config
+        nodes can use those configuration values in parameter expressions
+        """
+        raise NotImplementedError()
+
+    def hardware_resource_definitions(self) -> Tuple[WorkerResourceDefinition, ...]:
+        """
+        get definitions of generic resources that workers can have
+        """
+        raise NotImplementedError()
+
+    def hardware_ban_timeout(self) -> float:
+        """
+        on certain submission errors we might want to ban hwid for some time, as it can be assumed
+        that consecutive submission attempts will result in the same error (like package resolution error)
+        If that happened - particular hardware may be "banned" from participating in scheduling for a certain time period.
+        this is that period (in seconds)
+        """
+        raise NotImplementedError()
+
+    def ping_intervals(self) -> Tuple[float, float, float, float]:
+        """
+        returns 4 floats that represent:
+        - interval for active workers (workers doing work)
+        - interval for idle workers
+        - interval for off/errored workers  (not really used since workers need to report back first)
+        - interval multiplier in dormant state of the scheduler
+        """
+        raise NotImplementedError()
+
+    def external_log_location(self) -> Optional[str]:
+        """
+        location where to store worker logs.
+        or None (should be default) - in that case logs are stored within the main db
+        """
+        raise NotImplementedError()
+
+    def legacy_server_address(self) -> Optional[Tuple[str, int]]:
+        """
+        (address, port) where legacy command server should be listening to
+        """
+        raise NotImplementedError()
+
+    def server_message_addresses(self) -> Tuple[Tuple[str, int], ...]:
+        """
+        a number of (address, port) pairs where message command processors should be listening to
+        """
+        raise NotImplementedError()
+
+    def server_ui_address(self) -> Tuple[str, int]:
+        """
+        address where UI server should be listening to
+        """
+        raise NotImplementedError()
+
+    def broadcast_interval(self) -> Optional[float]:
+        """
+        scheduler may broadcast information about which addresses it listens to
+
+        None -> broadcasting is disabled
+        otherwise - interval for broadcasts in seconds
+        """
+        raise NotImplementedError()
+
+    def invocation_attempts(self) -> int:
+        """
+        how many times can invocation fail
+        """
+        raise NotImplementedError()
+
+    def task_processor_housekeeping_interval(self) -> float:
+        """
+        interval in seconds, when to perform cleanup of potentially dangling data
+        this process is not supposed to be run often, and should take little time to complete
+        """
+        raise NotImplementedError()
+
+    def ignore_node_deserialization_failures(self) -> bool:
+        """
+        if such errors are ignored - failed nodes will be recreated
+        this is generally NOT desired, but can help fix very broken databases
+        """
+        raise NotImplementedError()
+
+    def scheduler_helpers_minimal(self) -> int:
+        """
+        how many scheduler helper processes to ensure are always available
+        values below 1 should not be legal
+        """
+        raise NotImplementedError()
+
+    # configuration of Node Data Provider
+    def node_data_provider_custom_plugins_path(self) -> Path:
+        """
+        path to a special package where scheduler stores overrides
+        This package always overrides everything else in case of conflict
+        """
+        raise NotImplementedError()
+
+    def node_data_provider_extra_plugin_paths(self) -> Tuple[Path, ...]:
+        """
+        extra plugins to load.
+        returns a number of paths where to search for packages
+        """
+        raise NotImplementedError()
diff --git a/src/lifeblood/scheduler_config_provider_default.py b/src/lifeblood/scheduler_config_provider_default.py
new file mode 100644
index 00000000..c95a06da
--- /dev/null
+++ b/src/lifeblood/scheduler_config_provider_default.py
@@ -0,0 +1,98 @@
+import os
+from pathlib import Path
+from .scheduler_config_provider_base import SchedulerConfigProviderBase
+from .worker_resource_definition import WorkerResourceDefinition, WorkerResourceDataType
+from . import defaults
+from .nethelpers import all_interfaces
+
+from typing import Optional, Tuple
+
+
+class SchedulerConfigProviderDefaults(SchedulerConfigProviderBase):
+    """
+    this subclass implements SOME default values for scheduler configuration
+    that subclasses of this class can fall back to
+    """
+
+    def main_database_location(self) -> str:
+        return os.path.join(os.getcwd(), 'main.db')  # just "main.db" in working directory
+
+    def main_database_connection_timeout(self) -> float:
+        return 30
+
+    def node_configuration(self, node_type_id: str) -> dict:
+        return {}
+
+    def hardware_resource_definitions(self) -> Tuple[WorkerResourceDefinition, ...]:
+        return (
+            WorkerResourceDefinition('cpu_count',
+                                     WorkerResourceDataType.SHARABLE_COMPUTATIONAL_UNIT,
+                                     'CPU core count',
+                                     'CPU count'),
+            WorkerResourceDefinition('cpu_mem',
+                                     WorkerResourceDataType.MEMORY_BYTES,
+                                     'RAM amount in bytes',
+                                     'CPU ram (GB)'),
+            WorkerResourceDefinition('gpu_count',
+                                     WorkerResourceDataType.SHARABLE_COMPUTATIONAL_UNIT,
+                                     'number of GPUs',
+                                     'GPU count'),  # TODO: get rid of these in defaults when devices are implemented
+            WorkerResourceDefinition('gpu_mem',
+                                     WorkerResourceDataType.MEMORY_BYTES,
+                                     'combined GPU memory in bytes',
+                                     'GPU ram (GB)'),
+        )
+
+    def hardware_ban_timeout(self) -> float:
+        return 10.0
+
+    def ping_intervals(self) -> Tuple[float, float, float, float]:
+        return (
+            10,  # interval for active workers (workers doing work)
+            30,  # interval for idle workers
+            60,  # interval for off/errored workers  (not really used since workers need to report back first)
+            5    # dormant state multiplier
+        )
+
+    def external_log_location(self) -> Optional[str]:
+        return None
+
+    def legacy_server_address(self) -> Optional[Tuple[str, int]]:
+        return '0.0.0.0', defaults.scheduler_port()  # use all ifaces by default
+
+    def server_message_addresses(self) -> Tuple[Tuple[str, int], ...]:
+        return tuple(
+            (iface_ip, defaults.scheduler_message_port()) for iface_ip in all_interfaces()
+        )
+
+    def server_ui_address(self) -> Tuple[str, int]:
+        return '0.0.0.0', defaults.ui_port()
+
+    def _config_do_broadcast(self) -> Optional[bool]:
+        return True
+
+    def _config_broadcast_interval(self) -> Optional[float]:
+        return 10.0
+
+    def broadcast_interval(self) -> Optional[float]:
+        do_broadcast = self._config_do_broadcast()
+        broadcast_interval = self._config_broadcast_interval()
+        return broadcast_interval if do_broadcast else None
+
+    def invocation_attempts(self) -> int:
+        return 3
+
+    def task_processor_housekeeping_interval(self) -> float:
+        return 60.0
+
+    def ignore_node_deserialization_failures(self) -> bool:
+        return False
+
+    def scheduler_helpers_minimal(self) -> int:
+        return 1
+
+    def node_data_provider_custom_plugins_path(self) -> Path:
+        return Path.cwd() / 'custom_plugins'
+
+    def node_data_provider_extra_plugin_paths(self) -> Tuple[Path, ...]:
+        return ()
diff --git a/src/lifeblood/scheduler_config_provider_file.py b/src/lifeblood/scheduler_config_provider_file.py
new file mode 100644
index 00000000..f85143b7
--- /dev/null
+++ b/src/lifeblood/scheduler_config_provider_file.py
@@ -0,0 +1,344 @@
+import os
+from pathlib import Path
+from lifeblood.scheduler_config_provider_default import SchedulerConfigProviderDefaults
+from lifeblood.logging import get_logger
+from .worker_resource_definition import WorkerResourceDefinition, WorkerResourceDataType
+from . import paths
+from .config import get_config
+from .nethelpers import all_interfaces
+from .exceptions import SchedulerConfigurationError
+from .config import create_default_user_config_file, get_local_scratch_path
+from .text import escape
+
+from typing import Dict, List, Mapping, Optional, Tuple
+
+
+class SchedulerConfigProviderFile(SchedulerConfigProviderDefaults):
+    """
+    this subclass implements SOME default values for scheduler configuration
+    that subclasses of this class can fall back to
+
+    Note: Plugin provider expects to own underlying configs, meaning:
+     - it SHOULD read all underlying files once to avoid inconsistencies if config is changed by outer forces
+     - Currently the config is sorta immutable.
+        - IF config reread/update is ever implemented - a mechanism to inform config users about the change must be designed!
+    """
+
+    @classmethod
+    def generate_default_config_text(cls) -> str:
+        dcp = SchedulerConfigProviderDefaults()
+        ping_interval, ping_idle_interval, ping_off_interval, dormant_ping_multiplier = dcp.ping_intervals()
+        return default_config.format(
+            server_ip='0.0.0.0',  # special case, this will be transformed into a proper ip:port pair tuple of tuples
+            server_port=dcp.legacy_server_address()[1],
+            ui_ip=dcp.server_ui_address()[0],
+            ui_port=dcp.server_ui_address()[1],
+            do_broadcast='true' if dcp.broadcast_interval() is not None else 'false',
+            broadcast_interval=dcp.broadcast_interval() or 0,
+            scratch_location=escape(get_local_scratch_path(), '\\"'),
+            hwid_ban_timeout=dcp.hardware_ban_timeout(),
+            ping_interval=ping_interval,
+            ping_idle_interval=ping_idle_interval,
+            ping_off_interval=ping_off_interval,
+            dormant_ping_multiplier=dormant_ping_multiplier,
+            store_logs_externally='true' if dcp.external_log_location() is not None else 'false',
+            store_logs_externally_location=dcp.external_log_location() or '/path/to/dir/where/to/store/logs',
+            invocation_attempts=dcp.invocation_attempts(),
+            housekeeping_interval=dcp.task_processor_housekeeping_interval(),
+            ignore_node_deserialization_failures='true' if dcp.ignore_node_deserialization_failures() else 'false',
+            minimum_idle_helpers=dcp.scheduler_helpers_minimal(),
+        )
+
+    @classmethod
+    def generate_default_config_file_if_needed(cls):
+        create_default_user_config_file('scheduler', cls.generate_default_config_text())
+
+    def __init__(
+            self,
+    ):
+        logger = get_logger('scheduler.config_provider')
+
+        self.__config = get_config('scheduler')
+        self.__nodes_config = get_config('scheduler.nodes')
+
+        # cache node config mappings to get them fast when processing context is needed
+        #  this is the lazy solution
+        self.__node_config_cache: Dict[str, dict] = {}
+
+        node_plugin_paths = []
+        for path_str in os.environ.get('LIFEBLOOD_PLUGIN_PATH', '').split(os.pathsep):
+            if path_str == '':  # skip empty paths
+                continue
+            path = Path(path_str)
+            if not path.is_absolute():
+                logger.warning(f'plugin path must be absolute, skipping "{path_str}"')
+                continue
+            node_plugin_paths.append(path)
+        self.__node_plugin_paths: Tuple[Path, ...] = tuple(node_plugin_paths)
+
+    def main_database_location(self) -> str:
+        return self.__config.get_option_noasync('core.database.path', str(paths.default_main_database_location()))
+
+    def node_configuration(self, node_type_id: str) -> Mapping:
+        if node_type_id not in self.__node_config_cache:
+
+            self.__node_config_cache[node_type_id] = {
+                **dict(self.__config.get_option_noasync(
+                    'nodes.globals',
+                    self.__config.get_option_noasync('scheduler.globals', {}))  # 'scheduler.globals' is deprecated name
+                ),
+                **dict(self.__nodes_config.get_option_noasync(f'{node_type_id}', {})),
+            }
+        return self.__node_config_cache[node_type_id]
+
+    def hardware_resource_definitions(self) -> Tuple[WorkerResourceDefinition, ...]:
+        # resource definitions
+        config_resources = self.__config.get_option_noasync('resource_definitions.per_machine', None)
+        if config_resources is None:  # use default resource definitions
+            return super().hardware_resource_definitions()
+
+        if not isinstance(config_resources, dict):
+            raise RuntimeError('bad config schema: resource_definitions.per_machine must be a mapping')  # TODO: turn into config schema error or smth
+        conf_2_type_mapping = {
+            'int': WorkerResourceDataType.GENERIC_INT,
+            'float': WorkerResourceDataType.GENERIC_FLOAT,
+            'number': WorkerResourceDataType.GENERIC_FLOAT,
+            'cpu': WorkerResourceDataType.SHARABLE_COMPUTATIONAL_UNIT,
+            'mem': WorkerResourceDataType.MEMORY_BYTES,
+            'memory': WorkerResourceDataType.MEMORY_BYTES,
+        }
+        res_defs = []
+        for res_name, res_data in config_resources.items():
+            if res_name.startswith('total_'):
+                raise RuntimeError('resource name cannot start with "total_"')  # TODO: turn into config schema error or smth
+            res_type = conf_2_type_mapping.get(res_data.get('type').lower(), None)
+            if res_type is None:
+                raise RuntimeError('resource type may be one of "int", "float", "number"')  # TODO: turn into config schema error or smth
+            res_defs.append(WorkerResourceDefinition(
+                res_name,
+                res_type,
+                res_data.get('description', ''),
+                res_data.get('label', res_name),
+            ))
+        return tuple(res_defs)
+
+    def hardware_ban_timeout(self) -> float:
+        return self.__config.get_option_noasync('data_access.hwid_ban_timeout', super().hardware_ban_timeout())
+
+    def ping_intervals(self) -> Tuple[float, float, float, float]:
+        def_vals = super().ping_intervals()
+        return (
+            self.__config.get_option_noasync('pinger.ping_interval', def_vals[0]),
+            self.__config.get_option_noasync('pinger.ping_idle_interval', def_vals[1]),
+            self.__config.get_option_noasync('pinger.ping_off_interval', def_vals[2]),
+            self.__config.get_option_noasync('pinger.dormant_ping_multiplier', def_vals[3]),
+        )
+
+    def external_log_location(self) -> Optional[str]:
+        use_external = self.__config.get_option_noasync('core.database.store_logs_externally', False)
+        path_external = self.__config.get_option_noasync('core.database.store_logs_externally_location', None)
+        if use_external and not path_external:
+            raise SchedulerConfigurationError('if store_logs_externally is set - store_logs_externally_location must be set too')
+        return path_external if use_external else None
+
+    def legacy_server_address(self) -> Optional[Tuple[str, int]]:
+        default_addr, default_port = super().legacy_server_address()
+        legacy_server_port = self.__config.get_option_noasync(
+            'core.legacy_server_port',
+            self.__config.get_option_noasync(
+                'core.server_port',
+                default_port
+            )
+        )
+        legacy_server_ip = self.__config.get_option_noasync('core.server_ip', default_addr)
+        return legacy_server_ip, legacy_server_port
+
+    @staticmethod
+    def _expand_catchall_address(address: str) -> List[str]:
+        ret = []
+        if address == '0.0.0.0':  # message_processor address must be addressable, no catchall
+            for iface_ip in all_interfaces():
+                ret.append(iface_ip)
+        else:
+            ret.append(address)
+        return ret
+
+    def server_message_addresses(self) -> Tuple[Tuple[str, int], ...]:
+        default_values = super().server_message_addresses()
+        # TODO: add ability to declare multiple addresses
+        ret = []
+        message_server_ip = self.__config.get_option_noasync('core.server_port', None)
+        message_server_port = self.__config.get_option_noasync('core.server_message_port', None)
+        if (len(default_values) == 0 and (message_server_ip is None or message_server_port is None)
+                or message_server_ip is None and message_server_port is None):
+            return default_values
+        if message_server_ip is None:
+            message_server_ip = default_values[0][0]
+        if message_server_port is None:
+            message_server_port = default_values[0][1]
+
+        for server_ip in self._expand_catchall_address(message_server_ip):
+            addr = (server_ip, message_server_port)
+            if addr not in ret:
+                ret.append(addr)
+        return tuple(ret)
+
+    def server_ui_address(self) -> Tuple[str, int]:
+        default_ip, default_port = super().server_ui_address()
+        ui_address = (
+            self.__config.get_option_noasync('core.ui_ip', default_ip),
+            self.__config.get_option_noasync('core.ui_port', default_port)
+        )
+        return ui_address
+
+    def _config_do_broadcast(self) -> bool:
+        return self.__config.get_option_noasync('core.broadcast', super()._config_do_broadcast())
+
+    def _config_broadcast_interval(self) -> float:
+        return self.__config.get_option_noasync('core.broadcast_interval', super()._config_broadcast_interval())
+
+    def invocation_attempts(self) -> int:
+        return self.__config.get_option_noasync('task_processor.invocation_attempts', super().invocation_attempts())
+
+    def task_processor_housekeeping_interval(self) -> float:
+        return self.__config.get_option_noasync('task_processor.housekeeping_interval', super().task_processor_housekeeping_interval())
+
+    def ignore_node_deserialization_failures(self) -> bool:
+        return self.__config.get_option_noasync('core.ignore_node_deserialization_failures', super().ignore_node_deserialization_failures())
+
+    def scheduler_helpers_minimal(self) -> int:
+        return self.__config.get_option_noasync('core.minimum_idle_helpers', super().scheduler_helpers_minimal())
+
+    def node_data_provider_custom_plugins_path(self) -> Path:
+        return paths.config_path('', 'custom_plugins')
+
+    def node_data_provider_extra_plugin_paths(self) -> Tuple[Path, ...]:
+        return self.__node_plugin_paths
+
+
+class SchedulerConfigProviderFileOverrides(SchedulerConfigProviderFile):
+    def __init__(
+            self,
+            *,
+            main_db_location: Optional[str] = None,
+            main_db_connection_timeout: Optional[float] = None,
+            legacy_server_address: Optional[Tuple[str, int]] = None,
+            message_processor_address: Optional[Tuple[str, int]] = None,
+            ui_address: Optional[Tuple[str, int]] = None,
+            do_broadcast: Optional[bool] = None,
+            broadcast_interval: Optional[float] = None,
+            minimal_idle_helpers: Optional[int] = None,
+    ):
+        super().__init__()
+        self.__main_db_location_override = main_db_location
+        self.__main_db_connection_timeout = main_db_connection_timeout
+        self.__do_broadcast_override = do_broadcast
+        self.__broadcast_interval_override = broadcast_interval
+        self.__minimal_idle_helpers = minimal_idle_helpers
+        self.__legacy_address = legacy_server_address
+        self.__message_processor_address = message_processor_address
+        self.__ui_address = ui_address
+
+    def main_database_location(self) -> str:
+        return self.__main_db_location_override or super().main_database_location()
+    
+    def main_database_connection_timeout(self) -> float:
+        return self.__main_db_connection_timeout or super().main_database_connection_timeout()
+    
+    def _config_do_broadcast(self) -> bool:
+        return self.__do_broadcast_override if self.__do_broadcast_override is not None else super()._config_do_broadcast()
+
+    def _config_broadcast_interval(self) -> float:
+        return self.__broadcast_interval_override if self.__broadcast_interval_override is not None else super()._config_broadcast_interval()
+
+    def scheduler_helpers_minimal(self) -> int:
+        return self.__minimal_idle_helpers or super().scheduler_helpers_minimal()
+
+    def legacy_server_address(self) -> Optional[Tuple[str, int]]:
+        return self.__legacy_address or super().legacy_server_address()
+
+    def server_message_addresses(self) -> Tuple[Tuple[str, int], ...]:
+        if self.__message_processor_address is None:
+            return super().server_message_addresses()
+        ret = []
+        for server_ip in self._expand_catchall_address(self.__message_processor_address[0]):
+            addr = (server_ip, self.__message_processor_address[1])
+            ret.append(addr)
+        return tuple(ret)
+
+    def server_ui_address(self) -> Tuple[str, int]:
+        return self.__ui_address or super().server_ui_address()
+
+
+default_config = '''
+[core]
+## you can uncomment stuff below to specify some static values
+## 
+# server_ip = "{server_ip}"
+# server_port = {server_port}
+# ui_ip = "{ui_ip}"
+# ui_port = {ui_port}
+
+## you can turn off scheduler broadcasting if you want to manually configure viewer and workers to connect
+## to a specific address
+# broadcast = {do_broadcast}
+# broadcast_interval = {broadcast_interval}
+
+## more fine-tuning options
+# ignore_node_deserialization_failures = {ignore_node_deserialization_failures}
+# minimum_idle_helpers = {minimum_idle_helpers}
+
+[core.database]
+## you can specify default database path, 
+##  but this can be overridden with command line argument --db-path
+# path = "/path/to/database.db"
+
+
+[nodes.globals]
+## entries from this section will be available to any node from config[key] 
+##
+## if you use more than 1 machine - you must change this to a network location shared among all workers
+## by default it's set to scheduler's machine local temp path, and will only work for 1 machine setup 
+global_scratch_location = "{scratch_location}"
+
+
+[data_access]
+# hwid_ban_timeout = {hwid_ban_timeout}
+
+
+[task_processor]
+# invocation_attempts = {invocation_attempts}
+# housekeeping_interval = {housekeeping_interval}
+
+[pinger]
+# ping_interval = {ping_interval}
+# ping_idle_interval = {ping_idle_interval}
+# ping_off_interval = {ping_off_interval}
+# dormant_ping_multiplier = {dormant_ping_multiplier}
+
+## uncomment line below to store task logs outside of the database
+##  it works in a way that all NEW logs will be saved according to settings below
+##  existing logs will be kept where they are
+##  external logs will ALWAYS be looked for in location specified by store_logs_externally_location
+##  so if you have ANY logs saved externally - you must keep store_logs_externally_location defined in the config, 
+##    or those logs will be inaccessible
+##  but you can safely move logs and change location in config accordingly, but be sure scheduler is not accessing them at that time
+# [core.database]
+# store_logs_externally = {store_logs_externally}
+# store_logs_externally_location = {store_logs_externally_location}
+
+
+# [resource_definitions.per_machine]
+## you can define custom per-machine resources.
+## default are cpu_count and cpu_mem that represents the number of CPU cores and main memory size.
+## If you override this value - ALL default definitions will be override, so you need to add them back
+## if you want to keep them
+# cpu_count.type = "cpu"
+# cpu_count.description = "CPU core count"
+# cpu_count.label = "CPU count"
+# cpu_mem.type = "mem"
+# cpu_mem.description = "RAM amount in bytes"
+# cpu_mem.label = "CPU ram (GB)"
+# my_resource1.type = "float"
+# my_resource1.description = "the amount of rubber ducks that fit into the chassis"
+'''
diff --git a/src/lifeblood/scheduler_message_processor.py b/src/lifeblood/scheduler_message_processor.py
index d50accc3..38da48fb 100644
--- a/src/lifeblood/scheduler_message_processor.py
+++ b/src/lifeblood/scheduler_message_processor.py
@@ -5,7 +5,7 @@
 from .taskspawn import TaskSpawn
 from .enums import WorkerState, WorkerType, SpawnStatus, InvocationState, InvocationMessageResult
 from .worker_messsage_processor import WorkerControlClient
-from .net_classes import WorkerResources
+from .hardware_resources import HardwareResources
 from .worker_metadata import WorkerMetadata
 from .net_messages.impl.tcp_simple_command_message_processor import TcpCommandMessageProcessor
 from .net_messages.impl.clients import CommandJsonMessageClient
@@ -130,7 +130,7 @@ async def _command_hello(self, args: dict, client: CommandJsonMessageClient, ori
         res_data = args['worker_res'].encode('latin1')
 
         self._logger.debug('command: worker hello %s', addr)
-        worker_hardware: WorkerResources = WorkerResources.deserialize(res_data)
+        worker_hardware: HardwareResources = HardwareResources.deserialize(res_data)
         await self.__scheduler.add_worker(addr, workertype, worker_hardware, assume_active=True, worker_metadata=metadata)
         await client.send_message_as_json({'db_uid': self.__scheduler.db_uid()})
 
@@ -390,7 +390,7 @@ async def report_invocation_progress(self, invocation_id: int, progress: float):
         reply = await self.__client.receive_message()
         assert (await reply.message_body_as_json()).get('ok', False), 'something is not ok'
 
-    async def say_hello(self, address_to_advertise: AddressChain, worker_type: WorkerType, worker_resources: WorkerResources, worker_metadata: WorkerMetadata) -> int:
+    async def say_hello(self, address_to_advertise: AddressChain, worker_type: WorkerType, worker_resources: HardwareResources, worker_metadata: WorkerMetadata) -> int:
         await self.__client.send_command('worker.hello', {
             'worker_addr': str(address_to_advertise),
             'worker_type': worker_type.value,
diff --git a/src/lifeblood/scheduler_task_protocol.py b/src/lifeblood/scheduler_task_protocol.py
index 1c6750ce..d3ccfd1d 100644
--- a/src/lifeblood/scheduler_task_protocol.py
+++ b/src/lifeblood/scheduler_task_protocol.py
@@ -9,7 +9,7 @@
 from . import invocationjob
 from .taskspawn import TaskSpawn
 from .enums import WorkerType, SpawnStatus, WorkerState
-from .net_classes import WorkerResources
+from .hardware_resources import HardwareResources
 from .worker_metadata import WorkerMetadata
 
 from typing import TYPE_CHECKING, Optional, Tuple
@@ -78,7 +78,7 @@ async def comm_hello():  # elif command == b'hello':
             addr = await read_string()
             workertype: WorkerType = WorkerType(struct.unpack('>I', await reader.readexactly(4))[0])
             reslength = struct.unpack('>Q', await reader.readexactly(8))[0]
-            worker_hardware: WorkerResources = WorkerResources.deserialize(await reader.readexactly(reslength))
+            worker_hardware: HardwareResources = HardwareResources.deserialize(await reader.readexactly(reslength))
             metadata = WorkerMetadata(await read_string())
             await self.__scheduler.add_worker(addr, workertype, worker_hardware, assume_active=True, worker_metadata=metadata)
             writer.write(struct.pack('>Q', self.__scheduler.db_uid()))
@@ -304,7 +304,7 @@ async def _pulse3way_(self):
         await self.__writer.drain()
         await self.__reader.readexactly(1)
 
-    async def say_hello(self, address_to_advertise: str, worker_type: WorkerType, worker_resources: WorkerResources, metadata: WorkerMetadata):
+    async def say_hello(self, address_to_advertise: str, worker_type: WorkerType, worker_resources: HardwareResources, metadata: WorkerMetadata):
         await self._ensure_conn_open()
         self.write_string('hello')
         self.write_string(address_to_advertise)
diff --git a/src/lifeblood/scheduler_ui_protocol.py b/src/lifeblood/scheduler_ui_protocol.py
index 1ea19653..d4c4f76d 100644
--- a/src/lifeblood/scheduler_ui_protocol.py
+++ b/src/lifeblood/scheduler_ui_protocol.py
@@ -435,7 +435,8 @@ async def comm_save_custom_node_settings():
             settings = await asyncio.get_event_loop().run_in_executor(None, pickle.loads, await reader.readexactly(datasize))
             data_provider = self.__scheduler.node_data_provider()
             try:
-                data_provider.add_settings_to_existing_package('custom_default', node_type_name, settings_name, settings)
+                top_package_path = data_provider.loaded_packages_paths()[0]
+                data_provider.add_settings_to_existing_package(top_package_path, node_type_name, settings_name, settings)
             except RuntimeError as e:
                 self.__logger.error(f'failed to add custom node settings: {str(e)}')
                 writer.write(b'\0')
@@ -451,7 +452,8 @@ async def comm_set_settings_default():
                 settings_name = None
             data_provider = self.__scheduler.node_data_provider()
             try:
-                data_provider.set_settings_as_default(node_type_name, settings_name)
+                top_package_path = data_provider.loaded_packages_paths()[0]
+                data_provider.set_settings_as_default_in_existing_package(top_package_path, node_type_name, settings_name)
             except RuntimeError:
                 self.__logger.error(f'failed to set node default settings: {str(e)}')
                 writer.write(b'\0')
@@ -1304,11 +1306,18 @@ def set_task_environment_resolver_arguments(self, task_id: int, env_args: Option
         w.flush()
         assert r.readexactly(1) == b'\1'
 
-    def set_worker_groups(self, worker_hwid: int, groups: Iterable[str]):
+    def set_worker_groups(self, worker_hwid: str, groups: Iterable[str]):
+        """
+        NOTE:
+            internal scheduler's HWIDs are integers,
+            but exposed outside they are strings (mostly to ease the compatibility pain of PySide not working well with 64bit unsigned ints)
+            so here we expect hwid to be str
+        """
         groups = list(groups)
         r, w = self.__connection.get_rw_pair()
         w.write_string('setworkergroups')
-        w.write(struct.pack('>QQ', worker_hwid, len(groups)))
+        hwid = int(worker_hwid)
+        w.write(struct.pack('>QQ', hwid, len(groups)))
         for group in groups:
             w.write_string(group)
         w.flush()
diff --git a/src/lifeblood/stock_nodes/alicevision/nodes/camera_init.py b/src/lifeblood/stock_nodes/alicevision/nodes/camera_init.py
index 5604bb95..99b9df84 100644
--- a/src/lifeblood/stock_nodes/alicevision/nodes/camera_init.py
+++ b/src/lifeblood/stock_nodes/alicevision/nodes/camera_init.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.enums import NodeParameterType
 from lifeblood_alicevision_modules.base_node import AlicevisionBaseNode
diff --git a/src/lifeblood/stock_nodes/alicevision/nodes/depth_map.py b/src/lifeblood/stock_nodes/alicevision/nodes/depth_map.py
index a1fabb6c..bf567693 100644
--- a/src/lifeblood/stock_nodes/alicevision/nodes/depth_map.py
+++ b/src/lifeblood/stock_nodes/alicevision/nodes/depth_map.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.enums import NodeParameterType
 from lifeblood_alicevision_modules.base_node import AlicevisionBaseNode
diff --git a/src/lifeblood/stock_nodes/alicevision/nodes/depth_map_filter.py b/src/lifeblood/stock_nodes/alicevision/nodes/depth_map_filter.py
index 6de3b70d..2d39313e 100644
--- a/src/lifeblood/stock_nodes/alicevision/nodes/depth_map_filter.py
+++ b/src/lifeblood/stock_nodes/alicevision/nodes/depth_map_filter.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.enums import NodeParameterType
 from lifeblood_alicevision_modules.base_node import AlicevisionBaseNode
diff --git a/src/lifeblood/stock_nodes/alicevision/nodes/feature_extraction.py b/src/lifeblood/stock_nodes/alicevision/nodes/feature_extraction.py
index 5d0fad09..639cb6b4 100644
--- a/src/lifeblood/stock_nodes/alicevision/nodes/feature_extraction.py
+++ b/src/lifeblood/stock_nodes/alicevision/nodes/feature_extraction.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.enums import NodeParameterType
 from lifeblood_alicevision_modules.base_node import AlicevisionBaseNode
diff --git a/src/lifeblood/stock_nodes/alicevision/nodes/feature_matching.py b/src/lifeblood/stock_nodes/alicevision/nodes/feature_matching.py
index 5148a9d1..fb4469ae 100644
--- a/src/lifeblood/stock_nodes/alicevision/nodes/feature_matching.py
+++ b/src/lifeblood/stock_nodes/alicevision/nodes/feature_matching.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.enums import NodeParameterType
 from lifeblood_alicevision_modules.base_node import AlicevisionBaseNode
diff --git a/src/lifeblood/stock_nodes/alicevision/nodes/image_matching.py b/src/lifeblood/stock_nodes/alicevision/nodes/image_matching.py
index e2fd7054..0cb58342 100644
--- a/src/lifeblood/stock_nodes/alicevision/nodes/image_matching.py
+++ b/src/lifeblood/stock_nodes/alicevision/nodes/image_matching.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.enums import NodeParameterType
 from lifeblood_alicevision_modules.base_node import AlicevisionBaseNode
diff --git a/src/lifeblood/stock_nodes/alicevision/nodes/mesh_filtering.py b/src/lifeblood/stock_nodes/alicevision/nodes/mesh_filtering.py
index 3763dbe4..64e13ee9 100644
--- a/src/lifeblood/stock_nodes/alicevision/nodes/mesh_filtering.py
+++ b/src/lifeblood/stock_nodes/alicevision/nodes/mesh_filtering.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.enums import NodeParameterType
 from lifeblood_alicevision_modules.base_node import AlicevisionBaseNode
diff --git a/src/lifeblood/stock_nodes/alicevision/nodes/meshing.py b/src/lifeblood/stock_nodes/alicevision/nodes/meshing.py
index 3f16a168..5d652656 100644
--- a/src/lifeblood/stock_nodes/alicevision/nodes/meshing.py
+++ b/src/lifeblood/stock_nodes/alicevision/nodes/meshing.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.enums import NodeParameterType
 from lifeblood_alicevision_modules.base_node import AlicevisionBaseNode
diff --git a/src/lifeblood/stock_nodes/alicevision/nodes/prepare_dense_scene.py b/src/lifeblood/stock_nodes/alicevision/nodes/prepare_dense_scene.py
index 9b9c9fc0..09a84fad 100644
--- a/src/lifeblood/stock_nodes/alicevision/nodes/prepare_dense_scene.py
+++ b/src/lifeblood/stock_nodes/alicevision/nodes/prepare_dense_scene.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.enums import NodeParameterType
 from lifeblood_alicevision_modules.base_node import AlicevisionBaseNode
diff --git a/src/lifeblood/stock_nodes/alicevision/nodes/sfmtransform.py b/src/lifeblood/stock_nodes/alicevision/nodes/sfmtransform.py
index 3e28cd76..3b97745a 100644
--- a/src/lifeblood/stock_nodes/alicevision/nodes/sfmtransform.py
+++ b/src/lifeblood/stock_nodes/alicevision/nodes/sfmtransform.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.enums import NodeParameterType
 from lifeblood_alicevision_modules.base_node import AlicevisionBaseNode
diff --git a/src/lifeblood/stock_nodes/alicevision/nodes/structure_from_motion.py b/src/lifeblood/stock_nodes/alicevision/nodes/structure_from_motion.py
index c59b800a..6e4000ba 100644
--- a/src/lifeblood/stock_nodes/alicevision/nodes/structure_from_motion.py
+++ b/src/lifeblood/stock_nodes/alicevision/nodes/structure_from_motion.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.enums import NodeParameterType
 from lifeblood_alicevision_modules.base_node import AlicevisionBaseNode
diff --git a/src/lifeblood/stock_nodes/alicevision/nodes/texturing.py b/src/lifeblood/stock_nodes/alicevision/nodes/texturing.py
index ba4f87c6..bd78ebc7 100644
--- a/src/lifeblood/stock_nodes/alicevision/nodes/texturing.py
+++ b/src/lifeblood/stock_nodes/alicevision/nodes/texturing.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.enums import NodeParameterType
 from lifeblood_alicevision_modules.base_node import AlicevisionBaseNode
diff --git a/src/lifeblood/stock_nodes/alicevision/python/3/lifeblood_alicevision_modules/base_node.py b/src/lifeblood/stock_nodes/alicevision/python/3/lifeblood_alicevision_modules/base_node.py
index 1d6ac8b4..401bb810 100644
--- a/src/lifeblood/stock_nodes/alicevision/python/3/lifeblood_alicevision_modules/base_node.py
+++ b/src/lifeblood/stock_nodes/alicevision/python/3/lifeblood_alicevision_modules/base_node.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 import shutil
 from pathlib import Path
 
diff --git a/src/lifeblood/stock_nodes/blender/nodes/blender_batch_render.py b/src/lifeblood/stock_nodes/blender/nodes/blender_batch_render.py
index becb19a0..fbcd70cb 100644
--- a/src/lifeblood/stock_nodes/blender/nodes/blender_batch_render.py
+++ b/src/lifeblood/stock_nodes/blender/nodes/blender_batch_render.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.enums import NodeParameterType
 from lifeblood.nodethings import ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
diff --git a/src/lifeblood/stock_nodes/blender/nodes/blender_script.py b/src/lifeblood/stock_nodes/blender/nodes/blender_script.py
index 0101aad3..ee016007 100644
--- a/src/lifeblood/stock_nodes/blender/nodes/blender_script.py
+++ b/src/lifeblood/stock_nodes/blender/nodes/blender_script.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.enums import NodeParameterType
 from lifeblood.nodethings import ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
diff --git a/src/lifeblood/stock_nodes/blender/presets/blender_render.lbp b/src/lifeblood/stock_nodes/blender/presets/blender_render.lbp
index 0b82f560..86caf841 100644
--- a/src/lifeblood/stock_nodes/blender/presets/blender_render.lbp
+++ b/src/lifeblood/stock_nodes/blender/presets/blender_render.lbp
@@ -120,8 +120,8 @@
             "type": "blender_batch_render",
             "name": "blender batch render",
             "parameters": {
-                "priority adjustment": {
-                    "name": "priority adjustment",
+                "__requirements__.priority_adjustment": {
+                    "name": "__requirements__.priority_adjustment",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -130,8 +130,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost": {
-                    "name": "worker cpu cost",
+                "__requirements__.f_min_res_0": {
+                    "name": "__requirements__.f_min_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -140,8 +140,8 @@
                     "expr": "task['requirements']['cpu']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost preferred": {
-                    "name": "worker cpu cost preferred",
+                "__requirements__.f_pref_res_0": {
+                    "name": "__requirements__.f_pref_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -150,8 +150,8 @@
                     "expr": "task['requirements']['cpu']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost": {
-                    "name": "worker mem cost",
+                "__requirements__.f_min_res_1": {
+                    "name": "__requirements__.f_min_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -160,8 +160,8 @@
                     "expr": "task['requirements']['cmem']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost preferred": {
-                    "name": "worker mem cost preferred",
+                "__requirements__.f_pref_res_1": {
+                    "name": "__requirements__.f_pref_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -170,8 +170,8 @@
                     "expr": "task['requirements']['cmem']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker groups": {
-                    "name": "worker groups",
+                "__requirements__.worker_groups": {
+                    "name": "__requirements__.worker_groups",
                     "type": {
                         "value": 3,
                         "__NodeParameterType__": "==3*E=="
@@ -180,8 +180,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker type": {
-                    "name": "worker type",
+                "__requirements__.worker_type": {
+                    "name": "__requirements__.worker_type",
                     "type": {
                         "value": 0,
                         "__NodeParameterType__": "==3*E=="
diff --git a/src/lifeblood/stock_nodes/ffmpeg.py b/src/lifeblood/stock_nodes/ffmpeg.py
index b111ec17..ba7bd9e7 100644
--- a/src/lifeblood/stock_nodes/ffmpeg.py
+++ b/src/lifeblood/stock_nodes/ffmpeg.py
@@ -3,7 +3,7 @@
 
 from math import sqrt, floor, ceil
 
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.uidata import NodeParameterType
 from lifeblood.invocationjob import InvocationJob
diff --git a/src/lifeblood/stock_nodes/file_watcher.py b/src/lifeblood/stock_nodes/file_watcher.py
index 2f2d102c..1c861ccf 100644
--- a/src/lifeblood/stock_nodes/file_watcher.py
+++ b/src/lifeblood/stock_nodes/file_watcher.py
@@ -2,7 +2,7 @@
 from math import sqrt, floor, ceil
 import re
 
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.uidata import NodeParameterType
 from lifeblood.processingcontext import ProcessingContext
diff --git a/src/lifeblood/stock_nodes/fileop.py b/src/lifeblood/stock_nodes/fileop.py
index c1d8d299..b19a8656 100644
--- a/src/lifeblood/stock_nodes/fileop.py
+++ b/src/lifeblood/stock_nodes/fileop.py
@@ -1,6 +1,6 @@
 import os
 import shutil
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.processingcontext import ProcessingContext
diff --git a/src/lifeblood/stock_nodes/filepattern.py b/src/lifeblood/stock_nodes/filepattern.py
index 2eee83da..0c886bb9 100644
--- a/src/lifeblood/stock_nodes/filepattern.py
+++ b/src/lifeblood/stock_nodes/filepattern.py
@@ -2,7 +2,7 @@
 import fnmatch
 import inspect
 from lifeblood.taskspawn import TaskSpawn
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.processingcontext import ProcessingContext
@@ -65,7 +65,7 @@ def __init__(self, name):
             ui.add_parameter('pattern', 'file pattern', NodeParameterType.STRING, '')
             ui.add_parameter('file type', 'type', NodeParameterType.INT, 1).add_menu((('files', 1), ('dirs', 2), ('files and dirs', 3)))
             ui.add_parameter('inherit attributes', 'inherit attributes from parent', NodeParameterType.STRING, '')
-            ui.parameter('worker type').set_value(WorkerType.SCHEDULER_HELPER.value)
+            ui.parameter('__requirements__.worker_type').set_value(WorkerType.SCHEDULER_HELPER.value)
 
     @classmethod
     def label(cls) -> str:
diff --git a/src/lifeblood/stock_nodes/framerange_splitter.py b/src/lifeblood/stock_nodes/framerange_splitter.py
index 1dcfb32d..6545f341 100644
--- a/src/lifeblood/stock_nodes/framerange_splitter.py
+++ b/src/lifeblood/stock_nodes/framerange_splitter.py
@@ -1,5 +1,5 @@
 from copy import copy
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.enums import NodeParameterType
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 
diff --git a/src/lifeblood/stock_nodes/houdini/nodes/copy_temp_hip.py b/src/lifeblood/stock_nodes/houdini/nodes/copy_temp_hip.py
index 0cd1d558..945201bc 100644
--- a/src/lifeblood/stock_nodes/houdini/nodes/copy_temp_hip.py
+++ b/src/lifeblood/stock_nodes/houdini/nodes/copy_temp_hip.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.enums import NodeParameterType, WorkerType
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
@@ -47,7 +47,7 @@ def __init__(self, name):
                 ui.add_parameter('mask hip path attribute', 'attribute name', NodeParameterType.STRING, "hipfile_orig")\
                     .append_visibility_condition(mask_hip, '==', True)
 
-                ui.parameter('worker type').set_value(WorkerType.SCHEDULER_HELPER.value)
+                ui.parameter('__requirements__.worker_type').set_value(WorkerType.SCHEDULER_HELPER.value)
 
     def process_task(self, context) -> ProcessingResult:
         src_path = context.param_value('hip path')
diff --git a/src/lifeblood/stock_nodes/houdini/nodes/hip_driver_renderer.py b/src/lifeblood/stock_nodes/houdini/nodes/hip_driver_renderer.py
index b3884ca9..911e0a6b 100644
--- a/src/lifeblood/stock_nodes/houdini/nodes/hip_driver_renderer.py
+++ b/src/lifeblood/stock_nodes/houdini/nodes/hip_driver_renderer.py
@@ -1,5 +1,5 @@
 from copy import copy
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.enums import NodeParameterType
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
@@ -66,8 +66,8 @@ def __init__(self, name):
             ui.add_parameter('attrs to extract', 'detail attributes to extract', NodeParameterType.STRING, '')
             ui.add_parameter('intrinsics to extract', 'detail intrinsics to extract', NodeParameterType.STRING, '')
 
-            ui.parameter('worker type').set_hidden(True)
-            ui.parameter('worker type').set_locked(True)
+            ui.parameter('__requirements__.worker_type').set_hidden(True)
+            ui.parameter('__requirements__.worker_type').set_locked(True)
 
     @staticmethod
     def _fescape(s):
diff --git a/src/lifeblood/stock_nodes/houdini/nodes/hip_ifd_generator.py b/src/lifeblood/stock_nodes/houdini/nodes/hip_ifd_generator.py
index 476e0972..75f4f16e 100644
--- a/src/lifeblood/stock_nodes/houdini/nodes/hip_ifd_generator.py
+++ b/src/lifeblood/stock_nodes/houdini/nodes/hip_ifd_generator.py
@@ -1,5 +1,5 @@
 from copy import copy
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.enums import NodeParameterType
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
@@ -45,8 +45,8 @@ def __init__(self, name):
             ui.add_parameter('ifd force inline', 'force inline ifd', NodeParameterType.BOOL, True)
             ui.add_parameter('attrs', 'attributes to copy to children', NodeParameterType.STRING, '')
 
-            ui.parameter('worker type').set_hidden(True)
-            ui.parameter('worker type').set_locked(True)
+            ui.parameter('__requirements__.worker_type').set_hidden(True)
+            ui.parameter('__requirements__.worker_type').set_locked(True)
 
     def process_task(self, context) -> ProcessingResult:
         """
diff --git a/src/lifeblood/stock_nodes/houdini/nodes/hip_script.py b/src/lifeblood/stock_nodes/houdini/nodes/hip_script.py
index 9a176923..bc9244bc 100644
--- a/src/lifeblood/stock_nodes/houdini/nodes/hip_script.py
+++ b/src/lifeblood/stock_nodes/houdini/nodes/hip_script.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.enums import NodeParameterType
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
@@ -45,8 +45,8 @@ def __init__(self, name):
                 ui.add_parameter('save hip path', '', NodeParameterType.STRING, '')\
                     .append_visibility_condition(save_hip, '==', True)
 
-            ui.parameter('worker type').set_hidden(True)
-            ui.parameter('worker type').set_locked(True)
+            ui.parameter('__requirements__.worker_type').set_hidden(True)
+            ui.parameter('__requirements__.worker_type').set_locked(True)
 
     def process_task(self, context) -> ProcessingResult:
         script = 'import os, hou\n'
diff --git a/src/lifeblood/stock_nodes/houdini/nodes/hip_usd_generator.py b/src/lifeblood/stock_nodes/houdini/nodes/hip_usd_generator.py
index c09dd6df..a2119ff2 100644
--- a/src/lifeblood/stock_nodes/houdini/nodes/hip_usd_generator.py
+++ b/src/lifeblood/stock_nodes/houdini/nodes/hip_usd_generator.py
@@ -1,5 +1,5 @@
 from copy import copy
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.enums import NodeParameterType
 from lifeblood.processingcontext import ProcessingContext
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
diff --git a/src/lifeblood/stock_nodes/houdini/nodes/husk.py b/src/lifeblood/stock_nodes/houdini/nodes/husk.py
index a3743898..3032cbe1 100644
--- a/src/lifeblood/stock_nodes/houdini/nodes/husk.py
+++ b/src/lifeblood/stock_nodes/houdini/nodes/husk.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.enums import NodeParameterType
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
@@ -47,8 +47,8 @@ def __init__(self, name):
             ui.add_parameter('image path', 'output image file path', NodeParameterType.STRING, "`task['outimage']`")
             ui.add_parameter('skip if exists', 'skip if result already exists', NodeParameterType.BOOL, False)
 
-            ui.parameter('worker type').set_hidden(True)
-            ui.parameter('worker type').set_locked(True)
+            ui.parameter('__requirements__.worker_type').set_hidden(True)
+            ui.parameter('__requirements__.worker_type').set_locked(True)
 
     def process_task(self, context) -> ProcessingResult:
         args = context.task_attributes()
diff --git a/src/lifeblood/stock_nodes/houdini/nodes/karma.py b/src/lifeblood/stock_nodes/houdini/nodes/karma.py
index f0b014c3..d3052ff8 100644
--- a/src/lifeblood/stock_nodes/houdini/nodes/karma.py
+++ b/src/lifeblood/stock_nodes/houdini/nodes/karma.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.enums import NodeParameterType
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
@@ -44,8 +44,8 @@ def __init__(self, name):
             ui.add_parameter('image path', 'output image file path', NodeParameterType.STRING, "`task['outimage']`")
             ui.add_parameter('skip if exists', 'skip if result already exists', NodeParameterType.BOOL, False)
 
-            ui.parameter('worker type').set_hidden(True)
-            ui.parameter('worker type').set_locked(True)
+            ui.parameter('__requirements__.worker_type').set_hidden(True)
+            ui.parameter('__requirements__.worker_type').set_locked(True)
 
     def process_task(self, context) -> ProcessingResult:
         args = context.task_attributes()
diff --git a/src/lifeblood/stock_nodes/houdini/nodes/mantra.py b/src/lifeblood/stock_nodes/houdini/nodes/mantra.py
index f6c3f243..fec84349 100644
--- a/src/lifeblood/stock_nodes/houdini/nodes/mantra.py
+++ b/src/lifeblood/stock_nodes/houdini/nodes/mantra.py
@@ -1,5 +1,5 @@
 from copy import copy
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.enums import NodeParameterType
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
@@ -33,8 +33,8 @@ def __init__(self, name):
             ui.add_parameter('image path', 'output image file path', NodeParameterType.STRING, "`task['outimage']`")
             ui.add_parameter('skip if exists', 'skip if result already exists', NodeParameterType.BOOL, False)
 
-            ui.parameter('worker type').set_hidden(True)
-            ui.parameter('worker type').set_locked(True)
+            ui.parameter('__requirements__.worker_type').set_hidden(True)
+            ui.parameter('__requirements__.worker_type').set_locked(True)
 
     def process_task(self, context) -> ProcessingResult:
         env = InvocationEnvironment()
diff --git a/src/lifeblood/stock_nodes/houdini/presets/karma.lbp b/src/lifeblood/stock_nodes/houdini/presets/karma.lbp
index efe246b5..1c634155 100644
--- a/src/lifeblood/stock_nodes/houdini/presets/karma.lbp
+++ b/src/lifeblood/stock_nodes/houdini/presets/karma.lbp
@@ -5,8 +5,8 @@
             "type": "hip_usd_generator",
             "name": "generate usd",
             "parameters": {
-                "priority adjustment": {
-                    "name": "priority adjustment",
+                "__requirements__.priority_adjustment": {
+                    "name": "__requirements__.priority_adjustment",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -15,8 +15,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost": {
-                    "name": "worker cpu cost",
+                "__requirements__.f_min_res_0": {
+                    "name": "__requirements__.f_min_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -25,8 +25,8 @@
                     "expr": "task['requirements']['cpu']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost preferred": {
-                    "name": "worker cpu cost preferred",
+                "__requirements__.f_pref_res_0": {
+                    "name": "__requirements__.f_pref_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -35,8 +35,8 @@
                     "expr": "task['requirements']['cpu']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost": {
-                    "name": "worker mem cost",
+                "__requirements__.f_min_res_1": {
+                    "name": "__requirements__.f_min_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -45,8 +45,8 @@
                     "expr": "task['requirements']['cmem']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost preferred": {
-                    "name": "worker mem cost preferred",
+                "__requirements__.f_pref_res_1": {
+                    "name": "__requirements__.f_pref_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -55,8 +55,8 @@
                     "expr": "task['requirements']['cmem']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker groups": {
-                    "name": "worker groups",
+                "__requirements__.worker_groups": {
+                    "name": "__requirements__.worker_groups",
                     "type": {
                         "value": 3,
                         "__NodeParameterType__": "==3*E=="
@@ -65,8 +65,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker type": {
-                    "name": "worker type",
+                "__requirements__.worker_type": {
+                    "name": "__requirements__.worker_type",
                     "type": {
                         "value": 0,
                         "__NodeParameterType__": "==3*E=="
@@ -385,8 +385,8 @@
             "type": "karma",
             "name": "karma render usds",
             "parameters": {
-                "priority adjustment": {
-                    "name": "priority adjustment",
+                "__requirements__.priority_adjustment": {
+                    "name": "__requirements__.priority_adjustment",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -395,8 +395,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost": {
-                    "name": "worker cpu cost",
+                "__requirements__.f_min_res_0": {
+                    "name": "__requirements__.f_min_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -405,8 +405,8 @@
                     "expr": "task['requirements_render']['cpu']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost preferred": {
-                    "name": "worker cpu cost preferred",
+                "__requirements__.f_pref_res_0": {
+                    "name": "__requirements__.f_pref_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -415,8 +415,8 @@
                     "expr": "task['requirements_render']['cpu']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost": {
-                    "name": "worker mem cost",
+                "__requirements__.f_min_res_1": {
+                    "name": "__requirements__.f_min_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -425,8 +425,8 @@
                     "expr": "task['requirements_render']['cmem']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost preferred": {
-                    "name": "worker mem cost preferred",
+                "__requirements__.f_pref_res_1": {
+                    "name": "__requirements__.f_pref_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -435,8 +435,8 @@
                     "expr": "task['requirements_render']['cmem']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker groups": {
-                    "name": "worker groups",
+                "__requirements__.worker_groups": {
+                    "name": "__requirements__.worker_groups",
                     "type": {
                         "value": 3,
                         "__NodeParameterType__": "==3*E=="
@@ -445,8 +445,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker type": {
-                    "name": "worker type",
+                "__requirements__.worker_type": {
+                    "name": "__requirements__.worker_type",
                     "type": {
                         "value": 0,
                         "__NodeParameterType__": "==3*E=="
diff --git a/src/lifeblood/stock_nodes/houdini/presets/mantra.lbp b/src/lifeblood/stock_nodes/houdini/presets/mantra.lbp
index 9f5e4e52..151e40eb 100644
--- a/src/lifeblood/stock_nodes/houdini/presets/mantra.lbp
+++ b/src/lifeblood/stock_nodes/houdini/presets/mantra.lbp
@@ -5,8 +5,8 @@
             "type": "hip_ifd_generator",
             "name": "generate ifds",
             "parameters": {
-                "priority adjustment": {
-                    "name": "priority adjustment",
+                "__requirements__.priority_adjustment": {
+                    "name": "__requirements__.priority_adjustment",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -15,8 +15,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost": {
-                    "name": "worker cpu cost",
+                "__requirements__.f_min_res_0": {
+                    "name": "__requirements__.f_min_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -25,8 +25,8 @@
                     "expr": "task['requirements']['cpu']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost preferred": {
-                    "name": "worker cpu cost preferred",
+                "__requirements__.f_pref_res_0": {
+                    "name": "__requirements__.f_pref_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -35,8 +35,8 @@
                     "expr": "task['requirements']['cpu']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost": {
-                    "name": "worker mem cost",
+                "__requirements__.f_min_res_1": {
+                    "name": "__requirements__.f_min_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -45,8 +45,8 @@
                     "expr": "task['requirements']['cmem']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost preferred": {
-                    "name": "worker mem cost preferred",
+                "__requirements__.f_pref_res_1": {
+                    "name": "__requirements__.f_pref_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -55,8 +55,8 @@
                     "expr": "task['requirements']['cmem']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker groups": {
-                    "name": "worker groups",
+                "__requirements__.worker_groups": {
+                    "name": "__requirements__.worker_groups",
                     "type": {
                         "value": 3,
                         "__NodeParameterType__": "==3*E=="
@@ -65,8 +65,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker type": {
-                    "name": "worker type",
+                "__requirements__.worker_type": {
+                    "name": "__requirements__.worker_type",
                     "type": {
                         "value": 0,
                         "__NodeParameterType__": "==3*E=="
@@ -395,8 +395,8 @@
             "type": "mantra",
             "name": "mantra render ifds",
             "parameters": {
-                "priority adjustment": {
-                    "name": "priority adjustment",
+                "__requirements__.priority_adjustment": {
+                    "name": "__requirements__.priority_adjustment",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -405,8 +405,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost": {
-                    "name": "worker cpu cost",
+                "__requirements__.f_min_res_0": {
+                    "name": "__requirements__.f_min_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -415,8 +415,8 @@
                     "expr": "task['requirements_render']['cpu']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost preferred": {
-                    "name": "worker cpu cost preferred",
+                "__requirements__.f_pref_res_0": {
+                    "name": "__requirements__.f_pref_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -425,8 +425,8 @@
                     "expr": "task['requirements_render']['cpu']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost": {
-                    "name": "worker mem cost",
+                "__requirements__.f_min_res_1": {
+                    "name": "__requirements__.f_min_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -435,8 +435,8 @@
                     "expr": "task['requirements_render']['cmem']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost preferred": {
-                    "name": "worker mem cost preferred",
+                "__requirements__.f_pref_res_1": {
+                    "name": "__requirements__.f_pref_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -445,8 +445,8 @@
                     "expr": "task['requirements_render']['cmem']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker groups": {
-                    "name": "worker groups",
+                "__requirements__.worker_groups": {
+                    "name": "__requirements__.worker_groups",
                     "type": {
                         "value": 3,
                         "__NodeParameterType__": "==3*E=="
@@ -455,8 +455,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker type": {
-                    "name": "worker type",
+                "__requirements__.worker_type": {
+                    "name": "__requirements__.worker_type",
                     "type": {
                         "value": 0,
                         "__NodeParameterType__": "==3*E=="
diff --git a/src/lifeblood/stock_nodes/houdini/python/3/lifeblood_stock_houdini_helpers/rop_base_node.py b/src/lifeblood/stock_nodes/houdini/python/3/lifeblood_stock_houdini_helpers/rop_base_node.py
index 3d56e8a8..42c10595 100644
--- a/src/lifeblood/stock_nodes/houdini/python/3/lifeblood_stock_houdini_helpers/rop_base_node.py
+++ b/src/lifeblood/stock_nodes/houdini/python/3/lifeblood_stock_houdini_helpers/rop_base_node.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.enums import NodeParameterType
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
@@ -31,8 +31,8 @@ def __init__(self, name):
                 ui.add_parameter('gen for skipped', 'generate children for skipped', NodeParameterType.BOOL, True).append_visibility_condition(skipparam, '==', True)
             ui.add_parameter('attrs', 'attributes to copy to children', NodeParameterType.STRING, '')
 
-            ui.parameter('worker type').set_hidden(True)
-            ui.parameter('worker type').set_locked(True)
+            ui.parameter('__requirements__.worker_type').set_hidden(True)
+            ui.parameter('__requirements__.worker_type').set_locked(True)
 
     def _take_parm_name(self, context) -> str:
         raise NotImplementedError()
diff --git a/src/lifeblood/stock_nodes/houdini_distributed_sim/nodes/houdini_distributed_tracker_runner.py b/src/lifeblood/stock_nodes/houdini_distributed_sim/nodes/houdini_distributed_tracker_runner.py
index 5daf3b18..75da9c77 100644
--- a/src/lifeblood/stock_nodes/houdini_distributed_sim/nodes/houdini_distributed_tracker_runner.py
+++ b/src/lifeblood/stock_nodes/houdini_distributed_sim/nodes/houdini_distributed_tracker_runner.py
@@ -1,5 +1,5 @@
 import uuid
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult, InvocationJob
 from lifeblood.invocationjob import InvocationRequirements
 from lifeblood.enums import NodeParameterType, WorkerType
diff --git a/src/lifeblood/stock_nodes/houdini_distributed_sim/nodes/houdini_distributed_tracker_stopper.py b/src/lifeblood/stock_nodes/houdini_distributed_sim/nodes/houdini_distributed_tracker_stopper.py
index 9eb4ce91..c93b2caf 100644
--- a/src/lifeblood/stock_nodes/houdini_distributed_sim/nodes/houdini_distributed_tracker_stopper.py
+++ b/src/lifeblood/stock_nodes/houdini_distributed_sim/nodes/houdini_distributed_tracker_stopper.py
@@ -1,6 +1,6 @@
 import sys
 
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult, InvocationJob
 from lifeblood.invocationjob import InvocationRequirements
 from lifeblood.enums import NodeParameterType, WorkerType
diff --git a/src/lifeblood/stock_nodes/houdini_distributed_sim/presets/distributed_slices.lbp b/src/lifeblood/stock_nodes/houdini_distributed_sim/presets/distributed_slices.lbp
index ef90164e..d22ef655 100644
--- a/src/lifeblood/stock_nodes/houdini_distributed_sim/presets/distributed_slices.lbp
+++ b/src/lifeblood/stock_nodes/houdini_distributed_sim/presets/distributed_slices.lbp
@@ -5,8 +5,8 @@
             "type": "hip_driver_renderer",
             "name": "run sim slice",
             "parameters": {
-                "priority adjustment": {
-                    "name": "priority adjustment",
+                "__requirements__.priority_adjustment": {
+                    "name": "__requirements__.priority_adjustment",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -15,8 +15,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost": {
-                    "name": "worker cpu cost",
+                "__requirements__.f_min_res_0": {
+                    "name": "__requirements__.f_min_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -25,8 +25,8 @@
                     "expr": "task['requirements']['cpu']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost preferred": {
-                    "name": "worker cpu cost preferred",
+                "__requirements__.f_pref_res_0": {
+                    "name": "__requirements__.f_pref_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -35,8 +35,8 @@
                     "expr": "task['requirements']['cpu']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost": {
-                    "name": "worker mem cost",
+                "__requirements__.f_min_res_1": {
+                    "name": "__requirements__.f_min_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -45,8 +45,8 @@
                     "expr": "task['requirements']['cmem']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost preferred": {
-                    "name": "worker mem cost preferred",
+                "__requirements__.f_pref_res_1": {
+                    "name": "__requirements__.f_pref_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -55,8 +55,8 @@
                     "expr": "task['requirements']['cmem']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker groups": {
-                    "name": "worker groups",
+                "__requirements__.worker_groups": {
+                    "name": "__requirements__.worker_groups",
                     "type": {
                         "value": 3,
                         "__NodeParameterType__": "==3*E=="
@@ -65,8 +65,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker type": {
-                    "name": "worker type",
+                "__requirements__.worker_type": {
+                    "name": "__requirements__.worker_type",
                     "type": {
                         "value": 0,
                         "__NodeParameterType__": "==3*E=="
diff --git a/src/lifeblood/stock_nodes/htoa/nodes/arnold.py b/src/lifeblood/stock_nodes/htoa/nodes/arnold.py
index 3db87e3d..14618664 100644
--- a/src/lifeblood/stock_nodes/htoa/nodes/arnold.py
+++ b/src/lifeblood/stock_nodes/htoa/nodes/arnold.py
@@ -1,4 +1,4 @@
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.enums import NodeParameterType
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
@@ -32,8 +32,8 @@ def __init__(self, name):
             ui.add_parameter('image path', 'output image file path', NodeParameterType.STRING, "`task['outimage']`")
             ui.add_parameter('skip if exists', 'skip if result already exists', NodeParameterType.BOOL, False)
 
-            ui.parameter('worker type').set_hidden(True)
-            ui.parameter('worker type').set_locked(True)
+            ui.parameter('__requirements__.worker_type').set_hidden(True)
+            ui.parameter('__requirements__.worker_type').set_locked(True)
 
     def process_task(self, context) -> ProcessingResult:
         args = context.task_attributes()
diff --git a/src/lifeblood/stock_nodes/htoa/nodes/hip_ass_generator.py b/src/lifeblood/stock_nodes/htoa/nodes/hip_ass_generator.py
index 0a85e253..8f7dc22f 100644
--- a/src/lifeblood/stock_nodes/htoa/nodes/hip_ass_generator.py
+++ b/src/lifeblood/stock_nodes/htoa/nodes/hip_ass_generator.py
@@ -1,5 +1,5 @@
 from copy import copy
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.enums import NodeParameterType
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
@@ -44,8 +44,8 @@ def __init__(self, name):
                 ui.add_parameter('gen for skipped', 'generate children for skipped', NodeParameterType.BOOL, True).append_visibility_condition(skipparam, '==', True)
             ui.add_parameter('attrs', 'attributes to copy to children', NodeParameterType.STRING, '')
 
-            ui.parameter('worker type').set_hidden(True)
-            ui.parameter('worker type').set_locked(True)
+            ui.parameter('__requirements__.worker_type').set_hidden(True)
+            ui.parameter('__requirements__.worker_type').set_locked(True)
 
     def process_task(self, context) -> ProcessingResult:
         """
diff --git a/src/lifeblood/stock_nodes/htoa/presets/htoa.lbp b/src/lifeblood/stock_nodes/htoa/presets/htoa.lbp
index 24f773f9..ff5d662a 100644
--- a/src/lifeblood/stock_nodes/htoa/presets/htoa.lbp
+++ b/src/lifeblood/stock_nodes/htoa/presets/htoa.lbp
@@ -5,8 +5,8 @@
             "type": "hip_ass_generator",
             "name": "generate ass files",
             "parameters": {
-                "priority adjustment": {
-                    "name": "priority adjustment",
+                "__requirements__.priority_adjustment": {
+                    "name": "__requirements__.priority_adjustment",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -15,8 +15,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost": {
-                    "name": "worker cpu cost",
+                "__requirements__.f_min_res_0": {
+                    "name": "__requirements__.f_min_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -25,8 +25,8 @@
                     "expr": "task['requirements']['cpu']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost preferred": {
-                    "name": "worker cpu cost preferred",
+                "__requirements__.f_pref_res_0": {
+                    "name": "__requirements__.f_pref_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -35,8 +35,8 @@
                     "expr": "task['requirements']['cpu']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost": {
-                    "name": "worker mem cost",
+                "__requirements__.f_min_res_1": {
+                    "name": "__requirements__.f_min_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -45,8 +45,8 @@
                     "expr": "task['requirements']['cmem']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost preferred": {
-                    "name": "worker mem cost preferred",
+                "__requirements__.f_pref_res_1": {
+                    "name": "__requirements__.f_pref_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -55,8 +55,8 @@
                     "expr": "task['requirements']['cmem']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker groups": {
-                    "name": "worker groups",
+                "__requirements__.worker_groups": {
+                    "name": "__requirements__.worker_groups",
                     "type": {
                         "value": 3,
                         "__NodeParameterType__": "==3*E=="
@@ -65,8 +65,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker type": {
-                    "name": "worker type",
+                "__requirements__.worker_type": {
+                    "name": "__requirements__.worker_type",
                     "type": {
                         "value": 0,
                         "__NodeParameterType__": "==3*E=="
@@ -385,8 +385,8 @@
             "type": "arnold",
             "name": "arnold render ass",
             "parameters": {
-                "priority adjustment": {
-                    "name": "priority adjustment",
+                "__requirements__.priority_adjustment": {
+                    "name": "__requirements__.priority_adjustment",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -395,8 +395,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost": {
-                    "name": "worker cpu cost",
+                "__requirements__.f_min_res_0": {
+                    "name": "__requirements__.f_min_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -405,8 +405,8 @@
                     "expr": "task['requirements_mantra']['cpu']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost preferred": {
-                    "name": "worker cpu cost preferred",
+                "__requirements__.f_pref_res_0": {
+                    "name": "__requirements__.f_pref_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -415,8 +415,8 @@
                     "expr": "task['requirements_mantra']['cpu']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost": {
-                    "name": "worker mem cost",
+                "__requirements__.f_min_res_1": {
+                    "name": "__requirements__.f_min_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -425,8 +425,8 @@
                     "expr": "task['requirements_mantra']['cmem']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost preferred": {
-                    "name": "worker mem cost preferred",
+                "__requirements__.f_pref_res_1": {
+                    "name": "__requirements__.f_pref_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -435,8 +435,8 @@
                     "expr": "task['requirements_mantra']['cmem']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker groups": {
-                    "name": "worker groups",
+                "__requirements__.worker_groups": {
+                    "name": "__requirements__.worker_groups",
                     "type": {
                         "value": 3,
                         "__NodeParameterType__": "==3*E=="
@@ -445,8 +445,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker type": {
-                    "name": "worker type",
+                "__requirements__.worker_type": {
+                    "name": "__requirements__.worker_type",
                     "type": {
                         "value": 0,
                         "__NodeParameterType__": "==3*E=="
diff --git a/src/lifeblood/stock_nodes/imagemagik.py b/src/lifeblood/stock_nodes/imagemagik.py
index 35a1098b..db3f7110 100644
--- a/src/lifeblood/stock_nodes/imagemagik.py
+++ b/src/lifeblood/stock_nodes/imagemagik.py
@@ -3,7 +3,7 @@
 import re
 from math import sqrt, floor, ceil
 
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.uidata import NodeParameterType
 from lifeblood.invocationjob import InvocationJob
diff --git a/src/lifeblood/stock_nodes/matrixclient/nodes/matrixnotifier.py b/src/lifeblood/stock_nodes/matrixclient/nodes/matrixnotifier.py
index 3c59df69..5dc48880 100644
--- a/src/lifeblood/stock_nodes/matrixclient/nodes/matrixnotifier.py
+++ b/src/lifeblood/stock_nodes/matrixclient/nodes/matrixnotifier.py
@@ -1,6 +1,6 @@
 import sys
 import os
-from lifeblood.basenode import BaseNode, ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import BaseNode, ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.enums import NodeParameterType
 from lifeblood.paths import config_path
 from typing import Iterable
diff --git a/src/lifeblood/stock_nodes/redshift/nodes/redshift.py b/src/lifeblood/stock_nodes/redshift/nodes/redshift.py
index 1c51911c..5e9be3c2 100644
--- a/src/lifeblood/stock_nodes/redshift/nodes/redshift.py
+++ b/src/lifeblood/stock_nodes/redshift/nodes/redshift.py
@@ -1,5 +1,5 @@
 from copy import copy
-from lifeblood.basenode import BaseNodeWithTaskRequirements
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements
 from lifeblood.enums import NodeParameterType
 from lifeblood.nodethings import ProcessingResult, ProcessingError
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
@@ -34,8 +34,8 @@ def __init__(self, name):
             ui.add_parameter('skip if exists', 'skip if result already exists', NodeParameterType.BOOL, False)
 
             # ui.parameter('')
-            ui.parameter('worker type').set_hidden(True)
-            ui.parameter('worker type').set_locked(True)
+            ui.parameter('__requirements__.worker_type').set_hidden(True)
+            ui.parameter('__requirements__.worker_type').set_locked(True)
 
     def process_task(self, context) -> ProcessingResult:
         args = context.task_attributes()
diff --git a/src/lifeblood/stock_nodes/stock_presets/presets/cache.lbp b/src/lifeblood/stock_nodes/stock_presets/presets/cache.lbp
index 5646b584..15a408fd 100644
--- a/src/lifeblood/stock_nodes/stock_presets/presets/cache.lbp
+++ b/src/lifeblood/stock_nodes/stock_presets/presets/cache.lbp
@@ -81,8 +81,8 @@
             "type": "hip_driver_renderer",
             "name": "hip_driver_renderer Hykap",
             "parameters": {
-                "priority adjustment": {
-                    "name": "priority adjustment",
+                "__requirements__.priority_adjustment": {
+                    "name": "__requirements__.priority_adjustment",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -91,8 +91,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost": {
-                    "name": "worker cpu cost",
+                "__requirements__.f_min_res_0": {
+                    "name": "__requirements__.f_min_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -101,8 +101,8 @@
                     "expr": "task['requirements']['cpu']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost preferred": {
-                    "name": "worker cpu cost preferred",
+                "__requirements__.f_pref_res_0": {
+                    "name": "__requirements__.f_pref_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -111,8 +111,8 @@
                     "expr": "task['requirements']['cpu']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost": {
-                    "name": "worker mem cost",
+                "__requirements__.f_min_res_1": {
+                    "name": "__requirements__.f_min_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -121,8 +121,8 @@
                     "expr": "task['requirements']['cmem']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost preferred": {
-                    "name": "worker mem cost preferred",
+                "__requirements__.f_pref_res_1": {
+                    "name": "__requirements__.f_pref_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -131,8 +131,8 @@
                     "expr": "task['requirements']['cmem']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker groups": {
-                    "name": "worker groups",
+                "__requirements__.worker_groups": {
+                    "name": "__requirements__.worker_groups",
                     "type": {
                         "value": 3,
                         "__NodeParameterType__": "==3*E=="
@@ -141,8 +141,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker type": {
-                    "name": "worker type",
+                "__requirements__.worker_type": {
+                    "name": "__requirements__.worker_type",
                     "type": {
                         "value": 0,
                         "__NodeParameterType__": "==3*E=="
diff --git a/src/lifeblood/stock_nodes/stock_presets/presets/wedge.lbp b/src/lifeblood/stock_nodes/stock_presets/presets/wedge.lbp
index 1478e564..ce1a9ad7 100644
--- a/src/lifeblood/stock_nodes/stock_presets/presets/wedge.lbp
+++ b/src/lifeblood/stock_nodes/stock_presets/presets/wedge.lbp
@@ -140,8 +140,8 @@
             "type": "hip_driver_renderer",
             "name": "hip_driver_renderer Hykap copy",
             "parameters": {
-                "priority adjustment": {
-                    "name": "priority adjustment",
+                "__requirements__.priority_adjustment": {
+                    "name": "__requirements__.priority_adjustment",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -150,8 +150,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost": {
-                    "name": "worker cpu cost",
+                "__requirements__.f_min_res_0": {
+                    "name": "__requirements__.f_min_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -160,8 +160,8 @@
                     "expr": "task['requirements']['cpu']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker cpu cost preferred": {
-                    "name": "worker cpu cost preferred",
+                "__requirements__.f_pref_res_0": {
+                    "name": "__requirements__.f_pref_res_0",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -170,8 +170,8 @@
                     "expr": "task['requirements']['cpu']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost": {
-                    "name": "worker mem cost",
+                "__requirements__.f_min_res_1": {
+                    "name": "__requirements__.f_min_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -180,8 +180,8 @@
                     "expr": "task['requirements']['cmem']['min']",
                     "__dataclass__": "ParamData"
                 },
-                "worker mem cost preferred": {
-                    "name": "worker mem cost preferred",
+                "__requirements__.f_pref_res_1": {
+                    "name": "__requirements__.f_pref_res_1",
                     "type": {
                         "value": 2,
                         "__NodeParameterType__": "==3*E=="
@@ -190,8 +190,8 @@
                     "expr": "task['requirements']['cmem']['pref']",
                     "__dataclass__": "ParamData"
                 },
-                "worker groups": {
-                    "name": "worker groups",
+                "__requirements__.worker_groups": {
+                    "name": "__requirements__.worker_groups",
                     "type": {
                         "value": 3,
                         "__NodeParameterType__": "==3*E=="
@@ -200,8 +200,8 @@
                     "expr": null,
                     "__dataclass__": "ParamData"
                 },
-                "worker type": {
-                    "name": "worker type",
+                "__requirements__.worker_type": {
+                    "name": "__requirements__.worker_type",
                     "type": {
                         "value": 0,
                         "__NodeParameterType__": "==3*E=="
diff --git a/src/lifeblood/stock_nodes/telegram_client/nodes/telegram_notifier.py b/src/lifeblood/stock_nodes/telegram_client/nodes/telegram_notifier.py
index 4d40b184..91ebb56b 100644
--- a/src/lifeblood/stock_nodes/telegram_client/nodes/telegram_notifier.py
+++ b/src/lifeblood/stock_nodes/telegram_client/nodes/telegram_notifier.py
@@ -1,6 +1,6 @@
 import sys
 import os
-from lifeblood.basenode import BaseNodeWithTaskRequirements, ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements, ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.enums import NodeParameterType
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
 from lifeblood.paths import config_path
diff --git a/src/lifeblood/stock_nodes/webserver/nodes/webserver.py b/src/lifeblood/stock_nodes/webserver/nodes/webserver.py
index 4e8a4b5e..cd612cb8 100644
--- a/src/lifeblood/stock_nodes/webserver/nodes/webserver.py
+++ b/src/lifeblood/stock_nodes/webserver/nodes/webserver.py
@@ -1,6 +1,6 @@
 import sys
 import os
-from lifeblood.basenode import BaseNodeWithTaskRequirements, ProcessingResult, ProcessingContext, ProcessingError
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements, ProcessingResult, ProcessingContext, ProcessingError
 from lifeblood.enums import NodeParameterType
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.paths import config_path
diff --git a/src/lifeblood/ui_protocol_data.py b/src/lifeblood/ui_protocol_data.py
index c39e4d28..be730728 100644
--- a/src/lifeblood/ui_protocol_data.py
+++ b/src/lifeblood/ui_protocol_data.py
@@ -8,6 +8,7 @@
 from .enums import TaskState, WorkerState, WorkerType, TaskGroupArchivedState, InvocationState
 from .worker_metadata import WorkerMetadata
 from dataclasses import dataclass
+from enum import Enum
 
 from typing import Dict, List, Tuple, Type, Optional, Set, Union
 
@@ -406,27 +407,74 @@ def deserialize(cls, stream: BufferedReader) -> "NodeGraphStructureData":
         return NodeGraphStructureData(db_uid, nodes, connections)
 
 
+class WorkerResourceType(Enum):
+    INT = 0
+    FLOAT = 1
+
+
 @dataclass
+class WorkerResource:
+    value: Union[int, float]
+    total: Union[int, float]
+    type: WorkerResourceType
+    name: str
+
+
 class WorkerResources(IBufferSerializable):
-    cpu_count: float
-    total_cpu_count: float
-    cpu_mem: int
-    total_cpu_mem: int
-    gpu_count: float
-    total_gpu_count: float
-    gpu_mem: int
-    total_gpu_mem: int
+    def __init__(self, resources: List[WorkerResource]):
+        self.__resources: List[WorkerResource] = resources
+
+    def __repr__(self):
+        return f'WorkerResources({repr(self.__resources)})'
+
+    def __len__(self):
+        return len(self.__resources)
+
+    def __getitem__(self, item):
+        return self.__resources[item]
+
+    def __iter__(self):
+        return iter(self.__resources)
+
+    def __contains__(self, item):
+        return item in self.__resources
+
+    def __eq__(self, other):
+        if not isinstance(other, WorkerResources):
+            return False
+        return self.__resources == other.__resources
 
     def serialize(self, stream: BufferedIOBase):
-        stream.write(struct.pack('>ddQQddQQ', self.cpu_count, self.total_cpu_count, self.cpu_mem, self.total_cpu_mem,
-                                 self.gpu_count, self.total_gpu_count, self.gpu_mem, self.total_gpu_mem))
+        stream.write(struct.pack('>Q', len(self.__resources)))
+        for res in self.__resources:
+            if res.type == WorkerResourceType.INT:
+                s = 'Q'
+            elif res.type == WorkerResourceType.FLOAT:
+                s = 'd'
+            else:
+                raise NotImplementedError()
+            stream.write(struct.pack(f'>B{s}{s}', res.type.value, res.value, res.total))
+            _serialize_string(res.name, stream)
 
     @classmethod
     def deserialize(cls, stream: BufferedReader) -> "WorkerResources":
-        cpu_count, total_cpu_count, cpu_mem, total_cpu_mem, \
-        gpu_count, total_gpu_count, gpu_mem, total_gpu_mem = struct.unpack('>ddQQddQQ', stream.readexactly(64))
-        return WorkerResources(cpu_count, total_cpu_count, cpu_mem, total_cpu_mem,
-                               gpu_count, total_gpu_count, gpu_mem, total_gpu_mem)
+        resources = []
+        num_res, = struct.unpack('>Q', stream.readexactly(8))
+        for i in range(num_res):
+            res_type_val, = struct.unpack('>B', stream.readexactly(1))
+            res_type = WorkerResourceType(res_type_val)
+            if res_type == WorkerResourceType.INT:
+                s = 'Q'
+                ss = 8
+            elif res_type == WorkerResourceType.FLOAT:
+                s = 'd'
+                ss = 8
+            else:
+                raise NotImplementedError()
+            value, total = struct.unpack(f'>{s}{s}', stream.readexactly(2 * ss))
+            name = _deserialize_string(stream)
+            resources.append(WorkerResource(value, total, res_type, name))
+        return WorkerResources(resources)
 
 
 @dataclass
diff --git a/src/lifeblood/uidata.py b/src/lifeblood/uidata.py
index ceda49d5..de73bd98 100644
--- a/src/lifeblood/uidata.py
+++ b/src/lifeblood/uidata.py
@@ -698,7 +698,7 @@ def parameter(self, name: str) -> Parameter:
                 return layout.parameter(name)
             except ParameterNotFound:
                 continue
-        raise ParameterNotFound(f'parameter {name} not found in layout hierarchy')
+        raise ParameterNotFound(f'parameter "{name}" not found in layout hierarchy')
 
     def visible(self) -> bool:
         return len(self.children()) != 0 and any(x.visible() for x in self.items())
diff --git a/src/lifeblood/worker.py b/src/lifeblood/worker.py
index b1a7e3b6..3fcfaf04 100644
--- a/src/lifeblood/worker.py
+++ b/src/lifeblood/worker.py
@@ -12,7 +12,7 @@
 import tempfile
 from . import logging
 from .nethelpers import get_addr_to, get_localhost, get_hostname
-from .net_classes import WorkerResources
+from .hardware_resources import HardwareResources
 from .worker_metadata import WorkerMetadata
 from .exceptions import WorkerNotAvailable, AlreadyRunning, \
     InvocationMessageWrongInvocationId, InvocationMessageAddresseeTimeout, InvocationCancelled
@@ -91,10 +91,12 @@ def __init__(self, scheduler_addr: AddressChain, *,
         self.__local_invocation_server_address_string: str = ''
 
         self.__local_shared_dir = config.get_option_noasync("local_shared_dir_path", os.path.join(tempfile.gettempdir(), 'lifeblood_worker', 'shared'))
-        self.__my_resources = WorkerResources(cpu_count=config.get_option_noasync('resources.cpu_count'),
-                                              cpu_mem=config.get_option_noasync('resources.cpu_mem'),
-                                              gpu_count=config.get_option_noasync('resources.gpu_count'),
-                                              gpu_mem=config.get_option_noasync('resources.gpu_mem'))
+        self.__my_resources = HardwareResources(
+            cpu_count=config.get_option_noasync('resources.cpu_count') or psutil.cpu_count(),
+            cpu_mem=config.get_option_noasync('resources.cpu_mem') or psutil.virtual_memory().total,
+            gpu_count=config.get_option_noasync('resources.gpu_count') or 0,
+            gpu_mem=config.get_option_noasync('resources.gpu_mem') or 0,
+        )
         self.__task_changing_state_lock = asyncio.Lock()
         self.__task_switching_event = asyncio.Event()  # this will signal invocation message waiters to cancel what they are doing
         self.__stop_lock = threading.Lock()
diff --git a/src/lifeblood/worker_resource_definition.py b/src/lifeblood/worker_resource_definition.py
new file mode 100644
index 00000000..a58e4cd9
--- /dev/null
+++ b/src/lifeblood/worker_resource_definition.py
@@ -0,0 +1,24 @@
+from dataclasses import dataclass
+from enum import Enum
+
+from typing import Union
+
+
+class WorkerResourceDataType(Enum):
+    """
+    used for both defining actual data type,
+    and a hint for UI how to better display the resource
+    """
+    GENERIC_FLOAT = 0
+    GENERIC_INT = 1
+    SHARABLE_COMPUTATIONAL_UNIT = 2
+    MEMORY_BYTES = 3
+
+
+@dataclass
+class WorkerResourceDefinition:
+    name: str
+    type: WorkerResourceDataType
+    description: str
+    label: str  # nicer looking user facing name
+    default: Union[float, int] = 0
diff --git a/src/lifeblood_viewer/connection_worker.py b/src/lifeblood_viewer/connection_worker.py
index 93586416..c9a7d81c 100644
--- a/src/lifeblood_viewer/connection_worker.py
+++ b/src/lifeblood_viewer/connection_worker.py
@@ -1021,8 +1021,10 @@ def unset_environment_resolver_arguments(self, task_id: int):
         return self.set_environment_resolver_arguments(task_id, None)
 
     # TODO: problem below will affect ALL 64 bit arguments, need to correct all other functions
-    @Slot(object, list)  # interestingly since int here is 64 bit - i have to mark signal as object, but then it doesn't connect unless i specify slot as object too.
-    def set_worker_groups(self, whwid: int, groups: List[str]):
+    # interestingly since int here is 64 bit - i have to mark signal as object, but then it doesn't connect unless i specify slot as object too.
+    # switched int to str, so viewer only works with HWIDs as strings
+    @Slot(object, list)
+    def set_worker_groups(self, whwid: str, groups: List[str]):
         logger.debug(f'set_worker_groups with {whwid}, {groups}')
         if not self.ensure_connected():
             return
diff --git a/src/lifeblood_viewer/graphics_items.py b/src/lifeblood_viewer/graphics_items.py
index cb87279a..e026151b 100644
--- a/src/lifeblood_viewer/graphics_items.py
+++ b/src/lifeblood_viewer/graphics_items.py
@@ -694,7 +694,7 @@ def __draw_single_item(self, item, size=(1.0, 1.0), drawing_widget=None):
 
                     menu_items_inv = self.__nodeui_menucache[param_name]['menu_items_inv']
                     menu_order_inv = self.__nodeui_menucache[param_name]['menu_order_inv']
-                    if item.is_readonly():
+                    if item.is_readonly() or item.is_locked():  # TODO: treat locked items somehow different, but for now it's fine
                         imgui.text(menu_items_inv[item.value()])
                         return
                     else:
@@ -702,7 +702,7 @@ def __draw_single_item(self, item, size=(1.0, 1.0), drawing_widget=None):
                         if changed:
                             new_item_val = menu_items[menu_order[val]]
                 else:
-                    if item.is_readonly():
+                    if item.is_readonly() or item.is_locked():  # TODO: treat locked items somehow different, but for now it's fine
                         imgui.text(f'{item.value()}')
                         return
                     param_type = item.type()
@@ -776,6 +776,7 @@ def __draw_single_item(self, item, size=(1.0, 1.0), drawing_widget=None):
 
             if changed or expr_changed:
                 scene: QGraphicsImguiScene = self.scene()
+                # TODO: op below may fail, so callback to display error should be provided
                 scene.change_node_parameter(self.get_id(), item,
                                             new_item_val if changed else ...,
                                             new_item_expression if expr_changed else ...)
diff --git a/src/lifeblood_viewer/nodeeditor.py b/src/lifeblood_viewer/nodeeditor.py
index b5871f92..ef58de3c 100644
--- a/src/lifeblood_viewer/nodeeditor.py
+++ b/src/lifeblood_viewer/nodeeditor.py
@@ -332,6 +332,12 @@ def show_message(self, message: str, duration: Optional[float] = None):
         self.__overlay_message.move(self.width()//2 - label_size.width()//2, max(self.height()//2, self.height()*29//30 - label_size.height()))
         self.__overlay_message.raise_()
 
+    def _generic_operation_finished_callback(self, _, result: OperationCompletionDetails):
+        if result.status == OperationCompletionStatus.PartialSuccess:
+            self.show_message(f'::warning::{result.details}')
+        elif result.status == OperationCompletionStatus.NotPerformed:
+            self.show_message(f'::error::{result.details}')
+
     def rescan_presets(self):
         self.__viewer_presets = {}
         for preset_base_path in self.__preset_scan_paths:
@@ -520,12 +526,7 @@ def undo(self):
 
     @Slot()
     def delete_selected(self):
-        def _on_finished(_, result: OperationCompletionDetails):
-            if result.status == OperationCompletionStatus.PartialSuccess:
-                self.show_message(f'::warning::{result.details}')
-            elif result.status == OperationCompletionStatus.NotPerformed:
-                self.show_message(f'::error::{result.details}')
-        self.__scene.delete_selected_nodes(callback=_on_finished)
+        self.__scene.delete_selected_nodes(callback=self._generic_operation_finished_callback)
 
     @Slot(str, str, QPointF)
     def get_snippet_from_scheduler_and_create_nodes(self, package: str, preset_name: str, pos: QPointF):
diff --git a/src/lifeblood_viewer/scene_ops.py b/src/lifeblood_viewer/scene_ops.py
index 5bc7d0b4..f7254f72 100644
--- a/src/lifeblood_viewer/scene_ops.py
+++ b/src/lifeblood_viewer/scene_ops.py
@@ -4,6 +4,7 @@
 from .ui_snippets import UiNodeSnippetData
 from .graphics_items import Node, NodeConnection
 from lifeblood.snippets import NodeSnippetData
+from lifeblood.uidata import ParameterLocked, ParameterReadonly
 from PySide2.QtCore import QPointF
 
 from typing import Callable, TYPE_CHECKING, Optional, List, Mapping, Tuple, Dict, Set, Iterable, Union, Any, Sequence
@@ -330,10 +331,18 @@ def _my_do_longop(self, longop: LongOperation):
         longop.set_op_status(None, 'change parameter value')
         node_id = self.__scene._session_node_id_to_id(self.__node_sid)
         param = self.__scene.get_node(node_id).get_nodeui().parameter(self.__param_name)
-        if self.__new_value is not ...:
-            param.set_value(self.__new_value)
-        if self.__new_expression is not ...:
-            param.set_expression(self.__new_expression)
+        try:
+            if self.__new_value is not ...:
+                param.set_value(self.__new_value)
+            if self.__new_expression is not ...:
+                param.set_expression(self.__new_expression)
+        except ParameterLocked:
+            self._set_result(OperationCompletionDetails(OperationCompletionStatus.NotPerformed, 'parameter is locked'))
+            return
+        except ParameterReadonly:
+            self._set_result(OperationCompletionDetails(OperationCompletionStatus.NotPerformed, 'parameter is read only'))
+            return
+        # TODO: currently possible errors on scheduler side are ignored, not good
         self.__scene._send_node_parameters_change(node_id, [param], LongOperationData(longop))
         node = self.__scene.get_node(node_id)
         if node:
diff --git a/src/lifeblood_viewer/undo_stack.py b/src/lifeblood_viewer/undo_stack.py
index 5c692af7..a7d04396 100644
--- a/src/lifeblood_viewer/undo_stack.py
+++ b/src/lifeblood_viewer/undo_stack.py
@@ -174,11 +174,11 @@ def doop(longop: LongOperation):
                 logger.exception(f'exception happened during do operation "{self}"')
                 success = False
             finally:
-                self._undo_stack()._operation_finalized(self, True, success)
-
-            if success and callback:
                 op_result = self._my_do_result()
                 assert op_result is not None
+                self._undo_stack()._operation_finalized(op=self, add_to_stack=op_result.status != OperationCompletionStatus.NotPerformed, success=success)
+
+            if success and callback:
                 callback(self, op_result)
 
         if self.__was_done:
diff --git a/src/lifeblood_viewer/widgets/worker_list.py b/src/lifeblood_viewer/widgets/worker_list.py
index 6bf51564..eaf22661 100644
--- a/src/lifeblood_viewer/widgets/worker_list.py
+++ b/src/lifeblood_viewer/widgets/worker_list.py
@@ -1,5 +1,6 @@
 from datetime import datetime
-from lifeblood.ui_protocol_data import UiData, WorkerData, WorkerBatchData
+from dataclasses import dataclass
+from lifeblood.ui_protocol_data import UiData, WorkerData, WorkerBatchData, WorkerResources, WorkerMetadata
 from lifeblood.enums import WorkerType, WorkerState
 from lifeblood.text import nice_memory_formatting
 from lifeblood.logging import get_logger
@@ -7,21 +8,20 @@
 from lifeblood_viewer.connection_worker import SchedulerConnectionWorker
 from lifeblood_viewer.models.multiple_sort_model import MultipleFilterSortProxyModel
 
-from PySide2.QtWidgets import QWidget, QTableView, QHBoxLayout, QVBoxLayout, QHeaderView, QMenu, QLineEdit
+from PySide2.QtWidgets import QWidget, QTableView, QTreeView, QHBoxLayout, QVBoxLayout, QHeaderView, QMenu, QLineEdit
 from PySide2.QtCore import Slot, Signal, Qt, QAbstractItemModel, QAbstractTableModel, QModelIndex, QSortFilterProxyModel, QAbstractProxyModel, QPoint, QObject
 from PySide2.QtGui import QColor
 
-from typing import Any, Dict, Iterable, List, Optional
+from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union
 
 
-_init_column_order = ('id', 'state', 'progress', 'task_id', 'metadata.hostname', 'last_address', 'cpu_count', 'cpu_mem', 'gpu_count', 'gpu_mem', 'groups', 'last_seen', 'worker_type')
+_init_column_order_prototype = ('id', 'state', 'progress', 'task_id', 'metadata.hostname', 'last_address', 'last_seen', '__resources__', 'groups', 'worker_type')
 
 
 class WorkerListWidget(QWidget):
     def __init__(self, worker: SchedulerConnectionWorker, parent=None):
         super(WorkerListWidget, self).__init__(parent, Qt.Tool)
-        self.__worker_list = QTableView()
-        self.__worker_list.verticalHeader().setDefaultSectionSize(10)
+        self.__worker_list = QTreeView()
         self.__worker_model = WorkerModel(worker, self)
 
         col_id = self.__worker_model.column_by_name('id')
@@ -38,14 +38,14 @@ def __init__(self, worker: SchedulerConnectionWorker, parent=None):
         self.__worker_list.setModel(self.__sort_model)
         # self.__worker_list.horizontalHeader().setSectionResizeMode(QHeaderView.ResizeToContents)  # this cause incredible lag with QSplitter
 
-        ico = _init_column_order
-        self.__worker_list.horizontalHeader().resizeSection(ico.index('id'), 64)
-        self.__worker_list.horizontalHeader().resizeSection(ico.index('state'), 64)
-        self.__worker_list.horizontalHeader().resizeSection(ico.index('progress'), 64)
-        self.__worker_list.horizontalHeader().resizeSection(ico.index('task_id'), 64)
-        self.__worker_list.horizontalHeader().resizeSection(ico.index('last_address'), 140)
-        self.__worker_list.horizontalHeader().resizeSection(ico.index('last_seen'), 135)
-        self.__worker_list.horizontalHeader().resizeSection(ico.index('worker_type'), 135)
+        ico = _init_column_order_prototype
+        self.__worker_list.header().resizeSection(ico.index('id'), 64)
+        self.__worker_list.header().resizeSection(ico.index('state'), 64)
+        self.__worker_list.header().resizeSection(ico.index('progress'), 64)
+        self.__worker_list.header().resizeSection(ico.index('task_id'), 64)
+        self.__worker_list.header().resizeSection(ico.index('last_address'), 200)
+        self.__worker_list.header().resizeSection(ico.index('worker_type'), 135)
+        self.__worker_list.header().resizeSection(ico.index('last_seen'), 140)
 
         self.__worker_list.setSortingEnabled(True)
         self.__worker_list.sortByColumn(0, Qt.AscendingOrder)
@@ -95,9 +95,32 @@ def show_context_menu(self, pos: QPoint):
         menu.popup(gpos)
 
 
-class WorkerModel(QAbstractTableModel):
-    __cols_with_totals = {'cpu_count', 'cpu_mem', 'gpu_count', 'gpu_mem'}
-    __mem_cols = {'cpu_mem', 'gpu_mem'}
+@dataclass
+class WorkerModelData:  # almost like WorkerData, but better for display
+    id: Union[int, str]
+    worker_resources: WorkerResources
+    hwid: str
+    last_address: str
+    last_seen_timestamp: int
+    state: Union[WorkerState, Tuple[WorkerState, str]]
+    type: Optional[WorkerType]
+    current_invocation_node_id: Optional[int]
+    current_invocation_task_id: Optional[int]
+    current_invocation_id: Optional[int]
+    current_invocation_progress: Optional[float]
+    groups: Set[str]
+    metadata: Optional[WorkerMetadata]
+
+    @classmethod
+    def from_worker_data(cls, data: WorkerData) -> "WorkerModelData":
+        kwargs = {}
+        for field in WorkerData.__dict__['__dataclass_fields__']:
+            value = getattr(data, field)
+            kwargs[field] = value
+        return WorkerModelData(**kwargs)
+
+
+class WorkerModel(QAbstractItemModel):
     SORT_ROLE = Qt.UserRole + 0
 
     group_update_requested = Signal(object, list)  # not int, cuz int in PySide is signed 32bit only
@@ -107,18 +130,71 @@ def __init__(self, worker: SchedulerConnectionWorker, parent=None):
         super(WorkerModel, self).__init__(parent)
         self.__logger = get_logger('viewer.worker_model')
         self.__scheduler_worker = worker
-        self.__workers: Dict[str, WorkerData] = {}  # address is the key
-        self.__order: List[str] = []
-        self.__inv_order: Dict[str, int] = {}
-        self.__cols = {'id': 'id', 'state': 'state', 'metadata.hostname': 'hostname', 'last_address': 'address', 'cpu_count': 'cpus', 'cpu_mem': 'mem',
-                       'gpu_count': 'gpus', 'gpu_mem': 'gmem', 'last_seen': 'last seen', 'worker_type': 'type',
-                       'progress': 'progress', 'groups': 'groups', 'task_id': 'task id'}
-        self.__cols_order = _init_column_order
-        self.__colname_to_index = {k: i for i, k in enumerate(self.__cols_order)}
+
+        self.__workers: Dict[int, WorkerModelData] = {}  # worker id -> worker data
+        self.__worker_resources_names: List[str] = []
+
+        #
+        self.__hwid_summaries: Dict[str, Optional[WorkerModelData]] = {}
+
+        #
+        self.__hwid_order: List[str] = []
+        self.__wid_order: Dict[str, List[int]] = {}
+
+        #
+        self.__cols = {}
+        self.__cols_order = []
+        self.__colname_to_index = {}
         assert len(self.__cols) == len(self.__cols_order)
+        #
+
+        self.__last_update_structure_resources = None
+
+        self.__maybe_update_structure()
 
         self.start()
 
+    def index(self, row: int, column: int, parent: QModelIndex=None):
+        if parent is None:
+            parent = QModelIndex()
+        return self.createIndex(row, column,
+                                self.__hwid_order[parent.row()] if parent.isValid() else None
+                                )
+
+    def parent(self, index: QModelIndex):
+        if not index.isValid() or index.internalPointer() is None:
+            return QModelIndex()
+        hwid = index.internalPointer()
+        return self.createIndex(self.__hwid_order.index(hwid), 0, None)
+
+    def __maybe_update_structure(self):
+        """
+        this update is updating data structure if resource definitions have changed.
+        appropriate qt events are emitted
+        """
+        if self.__worker_resources_names == self.__last_update_structure_resources:
+            return
+
+        if self.__last_update_structure_resources is not None:  # None is only on initial update/init
+            self.__logger.debug("worker resource definitions changed, updating data structure")
+        self.beginResetModel()
+        try:
+            self.__cols_order = list(_init_column_order_prototype)
+            i = self.__cols_order.index('__resources__')
+            self.__cols_order = self.__cols_order[:i] + self.__worker_resources_names + self.__cols_order[i+1:]
+
+            self.__cols = {'id': 'id', 'state': 'state', 'metadata.hostname': 'hostname', 'last_address': 'address',
+                           'last_seen': 'last seen', 'worker_type': 'type',
+                           'progress': 'progress', 'groups': 'groups', 'task_id': 'task id'}
+
+            self.__cols.update({k: k.replace('_', ' ') for k in self.__worker_resources_names})
+
+            self.__colname_to_index = {k: i for i, k in enumerate(self.__cols_order)}
+            assert len(self.__cols) == len(self.__cols_order)
+        finally:
+            self.endResetModel()
+        self.__last_update_structure_resources = list(self.__worker_resources_names)
+
     def column_by_name(self, name) -> int:
         return self.__colname_to_index[name]
 
@@ -130,14 +206,59 @@ def headerData(self, section: int, orientation, role: int = Qt.DisplayRole):
         return self.__cols[self.__cols_order[section]]
 
     def columnCount(self, parent: QModelIndex = None) -> int:
-        return len(self.__cols)
+        return len(self.__cols_order)
 
     def rowCount(self, parent: QModelIndex = None) -> int:
-        return len(self.__workers)
+        if parent is None or not parent.isValid():  # root
+            return len(self.__hwid_order)
+        if parent.internalPointer() is not None:  # wid level, has no children
+            return 0
+        return len(self.__wid_order[self.__hwid_order[parent.row()]])
+
+    def __generate_summary(self, hwid):
+        workers = [self.__workers[x] for x in self.__wid_order[hwid]]
+        # make a pseudo worker statistical entry
+        state = WorkerState.UNKNOWN
+        if len(workers):
+            state = max((x.state for x in workers), key=lambda state: {
+                    WorkerState.ERROR: 10,  # just sorting order
+                    WorkerState.BUSY: 5,
+                    WorkerState.INVOKING: 4,
+                    WorkerState.IDLE: 1,
+                }.get(state, 0))
+
+        w_instate_count = 0
+        w_total_count = 0
+        summ_address = workers[0].last_address.rsplit('|', 1)[0] if len(workers) else 'unknown'
+        for worker in workers:
+            if worker.state == state:
+                w_instate_count += 1
+            if worker.state != WorkerState.OFF:
+                w_total_count += 1
+            # address
+            if summ_address != worker.last_address.rsplit('|', 1)[0]:
+                summ_address = '<multiple>'
+
+
+        return WorkerModelData(
+            '',
+            workers[0].worker_resources if len(workers) else WorkerResources([]),
+            hwid,
+            summ_address,
+            max(x.last_seen_timestamp for x in workers) if len(workers) else 0.0,
+            (state, f'{w_instate_count}/{w_total_count}') if len(workers) else state,
+            None,
+            None,
+            None,
+            None,
+            min(x.current_invocation_progress or 100 for x in workers) if len(workers) else None,
+            workers[0].groups if len(workers) else (),
+            workers[0].metadata if len(workers) else None,
+        )
 
     def data(self, index: QModelIndex, role: int = Qt.DisplayRole):
         def format_display(col_name, raw):
-            if col_name in self.__mem_cols:
+            if col_name.endswith('_mem'):
                 return nice_memory_formatting(raw)
             return raw
 
@@ -148,27 +269,37 @@ def format_display(col_name, raw):
         row = index.row()
         col = index.column()
         col_name = self.__cols_order[col]
-        worker = self.__workers[self.__order[row]]
+        if not index.parent().isValid():  # first level (hwid)
+            hwid = self.__hwid_order[row]
+            if self.__hwid_summaries.get(hwid, None) is None:
+                self.__hwid_summaries[hwid] = self.__generate_summary(hwid)
+            worker = self.__hwid_summaries[hwid]
+        else:
+            worker = self.__workers[self.__wid_order[self.__hwid_order[index.parent().row()]][row]]
 
         if col_name == 'id':
             raw_data = worker.id
             return raw_data
         if col_name == 'state':
-            data = worker.state
+            if isinstance(worker.state, tuple):
+                state, extra = worker.state
+            else:
+                state = worker.state
+                extra = None
             if role == Qt.DisplayRole:
-                return data.name
+                return f'{state.name} {extra}' if extra else state.name
             elif role == Qt.BackgroundRole:
-                if data in (WorkerState.BUSY, WorkerState.INVOKING):
+                if state in (WorkerState.BUSY, WorkerState.INVOKING):
                     return QColor.fromRgb(255, 255, 0, 64)
-                if data == WorkerState.IDLE:
+                if state == WorkerState.IDLE:
                     return QColor.fromRgb(0, 255, 0, 64)
-                if data == WorkerState.OFF:
+                if state == WorkerState.OFF:
                     return QColor.fromRgb(0, 0, 0, 64)
-                if data == WorkerState.ERROR:
+                if state == WorkerState.ERROR:
                     return QColor.fromRgb(255, 0, 0, 64)
                 return None
             elif role == self.SORT_ROLE:  # for sorting
-                return data.value
+                return state.value
         if col_name.startswith('metadata.'):
             metafield = col_name.split('.', 1)[1]
             return getattr(worker.metadata, metafield)
@@ -176,7 +307,7 @@ def format_display(col_name, raw):
             data = worker.current_invocation_progress
             if role == self.SORT_ROLE:  # for sorting
                 return data
-            if worker.state == WorkerState.BUSY and data is not None:
+            if (worker.state == WorkerState.BUSY or isinstance(worker.state, tuple) and worker.state[0] == WorkerState.BUSY) and data is not None:
                 return f'{data}%'
             return ''
         if col_name == 'task_id':
@@ -190,11 +321,17 @@ def format_display(col_name, raw):
                 return worker.last_seen_timestamp
             return datetime.fromtimestamp(worker.last_seen_timestamp).strftime('%H:%M:%S %d.%m.%Y')
         if col_name == 'worker_type':
+            if worker.type is None:
+                return None
             return worker.type.name
 
         raw_data = 'none'
-        if col_name in self.__cols_with_totals:  # if there's total - make value in format if val/total, like 20/32
-            raw_data = f'{format_display(col_name, getattr(worker.worker_resources, col_name))}/{format_display(col_name, getattr(worker.worker_resources, f"total_{col_name}"))}'
+        if col_name in self.__worker_resources_names:
+            i = self.__worker_resources_names.index(col_name)
+            if len(worker.worker_resources) <= i or worker.worker_resources[i].name != col_name:  # if data is called after worker_resource_names update, but before actual worker data update:
+                raw_data = '...'
+            else:
+                raw_data = f'{format_display(col_name, worker.worker_resources[i].value)}/{format_display(col_name, worker.worker_resources[i].total)}'
 
         return raw_data
 
@@ -205,6 +342,7 @@ def flags(self, index: QModelIndex) -> Qt.ItemFlags:
         return flags
 
     def setData(self, index: QModelIndex, value: Any, role: int = Qt.DisplayRole) -> bool:
+        # FOR NOW THIS ONLY SETS groups
         print(f'!setting {role}')
         if not index.isValid():
             return False
@@ -213,7 +351,11 @@ def setData(self, index: QModelIndex, value: Any, role: int = Qt.DisplayRole) ->
         row = index.row()
         col = index.column()
         col_name = self.__cols_order[col]
-        hwid = self.__workers[self.__order[row]]['hwid']
+        if not index.parent().isValid():  # first level (hwid)
+            hwid = self.__hwid_order[row]
+        else:
+            hwid = self.__hwid_order[index.parent().row()]
+            #hwid = self.__workers[self.__order[row]].hwid
         print(f'setting for {hwid}')
         if isinstance(value, str):
             groups = [y for y in (x.strip() for x in value.split(',')) if y != '']
@@ -228,20 +370,30 @@ def setData(self, index: QModelIndex, value: Any, role: int = Qt.DisplayRole) ->
     def cancel_running_invocations(self, index: QModelIndex):
         if not index.isValid():
             return
+        if not index.parent().isValid():  # first level (hwid)
+            return
         row = index.row()
-        wid = self.__workers[self.__order[row]].id
+        wid = self.__wid_order[self.__hwid_order[index.parent().row()]][row]
+        #wid = self.__workers[self.__order[row]].id
         self.cancel_invocation_for_worker.emit(wid)
 
     @Slot(object)
     def workers_full_update(self, workers_data: WorkerBatchData):
+        if len(workers_data.workers) == 0:
+            return
+        # update resource definitions
+        any_worker = next(iter(workers_data.workers.values()))
+        self.__worker_resources_names = [x.name for x in any_worker.worker_resources]
+        self.__maybe_update_structure()
+
         # we filter UNKNOWNS here, currently i see no point
         # displaying them any time.
         # maybe unknowns should not be returned by update at all?
         with performance_measurer() as pm:
-            new_workers: Dict[str, WorkerData] = {
-                x.last_address: x for x in workers_data.workers.values()
+            new_workers: Dict[int, WorkerData] = {
+                x_id: x for x_id, x in workers_data.workers.items()
                 if x.state != WorkerState.UNKNOWN
-            }  # TODO: maybe use id instead of last_address?
+            }
             new_keys = set(new_workers.keys())
             old_keys = set(self.__workers.keys())
         _perf_preinit = pm.elapsed()
@@ -249,54 +401,114 @@ def workers_full_update(self, workers_data: WorkerBatchData):
         # update
         _perf_signals = 0
         with performance_measurer() as pm:
-            minrow, maxrow = self.rowCount(), -1
+            rows_to_update = {}
+            workers_changed_hwid = []
             for worker_key in old_keys.intersection(new_keys):
-                # self.__workers[wroker_key].update(new_workers[worker_key])
                 for field in WorkerData.__dict__['__dataclass_fields__']:
                     value = getattr(new_workers[worker_key], field)
-                    if getattr(self.__workers[worker_key], field) != value:
+                    old_value = getattr(self.__workers[worker_key], field)
+                    if old_value != value:
                         setattr(self.__workers[worker_key], field, value)
-                        row = self.__inv_order[worker_key]
-                        if minrow > row:
-                            minrow = row
-                        if maxrow < row:
-                            maxrow = row
-                        # colindex = self.__colname_to_index.get(key)
-                        # with performance_measurer() as pm1:
-                        #     if colindex is not None:
-                        #         self.dataChanged.emit(self.index(self.__inv_order[worker_key], colindex), self.index(self.__inv_order[worker_key], colindex))
-                        # _perf_signals += pm1.elapsed()
-            if minrow > -1:
-                # individual emits were VERY slow, even emiting once for ALL data is significantly faster, so we do this.
-                self.dataChanged.emit(self.index(minrow, 0), self.index(maxrow, self.columnCount()-1))  # TODO: this is very crude method of minimizing emit calls. IMPROVE
+                        hwid = self.__workers[worker_key].hwid
+                        wid = self.__workers[worker_key].id
+                        hwid_row = self.__hwid_order.index(hwid)
+                        wid_row = self.__wid_order[hwid].index(wid)
+                        if hwid_row not in rows_to_update:
+                            rows_to_update[hwid_row] = [wid_row, wid_row]
+                        else:
+                            rows_to_update[hwid_row][0] = min(wid_row, rows_to_update[hwid_row][0])
+                            rows_to_update[hwid_row][1] = max(wid_row, rows_to_update[hwid_row][1])
+                        if field == 'hwid':
+                            assert value == self.__workers[worker_key].hwid
+                            workers_changed_hwid.append([old_value, value, self.__workers[worker_key]])
+                        if hwid in self.__hwid_summaries:  # reset summary cache
+                            self.__hwid_summaries.pop(hwid)
+
+            # emit update signal
+            if len(rows_to_update) > 0:
+                self.dataChanged.emit(self.index(min(rows_to_update), 0), self.index(max(rows_to_update), self.columnCount()-1))
+                for hwid_row, (wid_row_min, wid_row_max) in rows_to_update.items():
+                    parent_index = self.index(hwid_row, 0)
+                    self.dataChanged.emit(self.index(wid_row_min, 0, parent_index), self.index(wid_row_max, self.columnCount(parent_index)-1, parent_index))
         _perf_update = pm.elapsed()
 
-        # insert
+        # note, at this point after update there MAY be workers in incorrect hwid parent (in case wid was reassigned by scheduler, that can rarely happen)
+        #  we have them in workers_changed_hwid list
+
+        # insert new
         with performance_measurer() as pm:
-            to_insert = new_keys - old_keys
-            if len(to_insert) > 0:
-                self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount() + len(to_insert) - 1)
-                for key in to_insert:
-                    assert key not in self.__workers
-                    assert key not in self.__inv_order
-                    self.__workers[key] = new_workers[key]
-                    self.__inv_order[key] = len(self.__order)
-                    self.__order.append(key)
+            hwid_to_new_worker = {}
+            for wid in new_keys - old_keys:
+                worker = new_workers[wid]
+                hwid_to_new_worker.setdefault(worker.hwid, []).append(worker)
+            for _, hwid, worker in workers_changed_hwid:
+                hwid_to_new_worker.setdefault(hwid, []).append(worker)
+            # first - insert new hwids
+            new_hwids = {x for x in hwid_to_new_worker if x not in self.__hwid_order}
+            if len(new_hwids) > 0:
+                self.beginInsertRows(QModelIndex(), self.rowCount(), self.rowCount() + len(new_hwids) - 1)
+                for hwid in new_hwids:
+                    self.__hwid_order.append(hwid)
+                    self.__wid_order[hwid] = []
+                self.endInsertRows()
+            # now that all hwids exist - insert children
+            for hwid, new_workers_for_hwid in hwid_to_new_worker.items():
+                if len(new_workers_for_hwid) == 0:
+                    continue
+                if hwid in self.__hwid_summaries:  # reset summary cache
+                    self.__hwid_summaries.pop(hwid)
+                parent_index = self.index(self.__hwid_order.index(hwid), 0)
+                self.beginInsertRows(parent_index, self.rowCount(parent_index), self.rowCount(parent_index) + len(new_workers_for_hwid) - 1)
+                for worker in new_workers_for_hwid:
+                    assert worker.id not in self.__workers
+                    self.__workers[worker.id] = WorkerModelData.from_worker_data(worker)
+                    self.__wid_order[hwid].append(worker.id)
                 self.endInsertRows()
+
+            # signal parent update
+            if len(hwid_to_new_worker) > 0:
+                rows_to_update = [self.__hwid_order.index(x) for x in hwid_to_new_worker]
+                self.dataChanged.emit(self.index(min(rows_to_update), 0), self.index(max(rows_to_update), self.columnCount() - 1))
+
         _perf_insert = pm.elapsed()
 
         # remove
         with performance_measurer() as pm:
-            to_remove = old_keys - new_keys
-            if len(to_remove) > 0:
-                for key in to_remove:
-                    assert key in self.__workers
-                    assert key in self.__inv_order
-                    self.beginRemoveRows(QModelIndex(), self.__inv_order[key], self.__inv_order[key])
-                    del self.__workers[key]
-                    self.__order.remove(key)
-                    self.__inv_order = {x: i for i, x in enumerate(self.__order)}
+            hwid_to_wid_to_remove = {}
+            for wid in old_keys - new_keys:
+                hwid = self.__workers[wid]
+                hwid_to_wid_to_remove.setdefault(hwid, []).append(wid)
+            for hwid, _, worker in workers_changed_hwid:
+                hwid_to_wid_to_remove.setdefault(hwid, []).append(worker.id)
+
+            hwids_to_remove = set()
+            hwids_to_update = set()
+            for hwid, wids_to_remove in hwid_to_wid_to_remove.items():
+                if len(wids_to_remove) == 0:
+                    continue
+                for wid in wids_to_remove:
+                    assert wid in self.__workers
+                    parent_index = self.index(self.__hwid_order.index(hwid), 0)
+                    self.beginRemoveRows(parent_index, self.__wid_order[hwid].index(wid), self.__wid_order[hwid].index(wid))
+                    self.__workers.pop(wid)
+                    self.__wid_order[hwid].remove(wid)
+                    if len(self.__wid_order[hwid]) == 0:
+                        hwids_to_remove.add(hwid)
+                    else:
+                        hwids_to_update.add(hwid)
                     self.endRemoveRows()
+            for hwid in hwids_to_remove:
+                assert len(self.__wid_order[hwid]) == 0
+                self.beginRemoveRows(QModelIndex(), self.__hwid_order.index(hwid), self.__hwid_order.index(hwid))
+                self.__hwid_order.remove(hwid)
+                self.endRemoveRows()
+            if len(hwids_to_update) > 0:
+                rows_to_update = [self.__hwid_order.index(x) for x in hwids_to_update]
+                self.dataChanged.emit(self.index(min(rows_to_update), 0), self.index(max(rows_to_update), self.columnCount() - 1))
+            for hwid in hwids_to_update:
+                if hwid in self.__hwid_summaries:  # reset summary cache
+                    self.__hwid_summaries.pop(hwid)
+
         _perf_remove = pm.elapsed()
 
         if _perf_preinit + _perf_update + _perf_signals + _perf_insert + _perf_remove > 0.04:  # arbitrary threshold ~ 1/25 of a sec


Test patch:
diff --git a/src/lifeblood/core_nodes/test.py b/src/lifeblood/core_nodes/test.py
index 8566c084..34922267 100644
--- a/src/lifeblood/core_nodes/test.py
+++ b/src/lifeblood/core_nodes/test.py
@@ -1,6 +1,6 @@
 import time
 
-from lifeblood.basenode import BaseNode
+from lifeblood.node_plugin_base import BaseNode
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
 from lifeblood.nodethings import ProcessingResult
 from lifeblood.uidata import NodeParameterType
diff --git a/src/lifeblood_testing_common/common.py b/src/lifeblood_testing_common/common.py
index 4b459b29..d70ab8a6 100644
--- a/src/lifeblood_testing_common/common.py
+++ b/src/lifeblood_testing_common/common.py
@@ -1,4 +1,55 @@
+from pathlib import Path
+import lifeblood
+from lifeblood.scheduler.scheduler import Scheduler
+from lifeblood.worker_resource_definition import WorkerResourceDefinition
+from lifeblood.basenode_serializer_v2 import NodeSerializerV2
+from lifeblood.pluginloader import PluginNodeDataProvider  # TODO: this must be replaced by a testing mocker
+from lifeblood_testing_common.scheduler_config_provider_default_override import SchedulerConfigProviderOverrides
+
+from typing import Any, Dict, Optional, Tuple
+
 
 async def chain(*coros):
     for coro in coros:
         await coro
+
+
+def create_default_scheduler(
+        db_file_path,
+        *,
+        do_broadcasting: Optional[bool] = None,
+        broadcast_interval: Optional[int] = None,
+        helpers_minimal_idle_to_ensure=1,
+        server_addr: Optional[Tuple[str, int, int]] = None,
+        server_ui_addr: Optional[Tuple[str, int]] = None,
+        node_per_node_config: Optional[Dict[str, Dict[str, Any]]] = None,
+        node_global_config: Optional[Dict[str, Dict[str, Any]]] = None,
+        resource_definitions: Optional[Tuple[WorkerResourceDefinition, ...]] = None,
+) -> Scheduler:
+    legacy_addr = None
+    message_addr = None
+    if server_addr is not None:
+        legacy_addr = (server_addr[0], server_addr[1])
+        message_addr = (server_addr[0], server_addr[2])
+    config = SchedulerConfigProviderOverrides(
+        main_db_location=db_file_path,
+        do_broadcast=do_broadcasting,
+        broadcast_interval=broadcast_interval,
+        minimal_idle_helpers=helpers_minimal_idle_to_ensure,
+        legacy_server_address=legacy_addr,
+        message_processor_address=message_addr,
+        ui_address=server_ui_addr,
+        node_per_node_config=node_per_node_config,
+        node_global_config=node_global_config,
+        resource_definitions=resource_definitions,
+    )
+    return Scheduler(
+        scheduler_config_provider=config,
+        node_data_provider=PluginNodeDataProvider(
+            plugin_paths=(
+                (Path(lifeblood.__file__).parent / 'stock_nodes', 'stock'),
+                (Path(lifeblood.__file__).parent / 'core_nodes', 'core'),
+            )
+        ),
+        node_serializers=[NodeSerializerV2()],
+    )
diff --git a/src/lifeblood_testing_common/integration_common.py b/src/lifeblood_testing_common/integration_common.py
index 5e016464..57d052c0 100644
--- a/src/lifeblood_testing_common/integration_common.py
+++ b/src/lifeblood_testing_common/integration_common.py
@@ -6,7 +6,7 @@
 import shutil
 from unittest import IsolatedAsyncioTestCase
 from lifeblood.enums import TaskState
-from lifeblood.main_scheduler import create_default_scheduler
+from lifeblood_testing_common.common import create_default_scheduler
 from lifeblood.nethelpers import get_default_addr
 from lifeblood.simple_worker_pool import WorkerPool
 from lifeblood.net_messages.address import AddressChain
@@ -34,6 +34,14 @@ class FullIntegrationTestCase(IsolatedAsyncioTestCaseWithDb):
     def this_test_dir(cls) -> Path:
         return Path(inspect.getmodule(cls).__file__).parent
 
+    @classmethod
+    def node_configs(cls) -> Tuple[dict, dict]:
+        """
+        first one - per-node
+        second one - global
+        """
+        return {}, {}
+
     async def asyncSetUp(self):
         db_name = self.db_file
         shutil.copy2(self.this_test_dir() / self._initial_db_file(), db_name)
@@ -41,11 +49,14 @@ async def asyncSetUp(self):
         test_server_port1 = 18273
         test_server_port2 = 18283
         test_server_port3 = 18293
+        c1, c2 = self.node_configs()
         self.scheduler = create_default_scheduler(
             db_name,
             do_broadcasting=False,
             server_addr=(get_default_addr(), test_server_port1, test_server_port2),
-            server_ui_addr=(get_default_addr(), test_server_port3)
+            server_ui_addr=(get_default_addr(), test_server_port3),
+            node_per_node_config=c1,
+            node_global_config=c2,
         )
         self.worker_pool = WorkerPool(
             scheduler_address=AddressChain(f'{get_default_addr()}:{test_server_port2}'),
diff --git a/src/lifeblood_testing_common/nodes_common.py b/src/lifeblood_testing_common/nodes_common.py
index fbcc7cdd..e5f91150 100644
--- a/src/lifeblood_testing_common/nodes_common.py
+++ b/src/lifeblood_testing_common/nodes_common.py
@@ -7,13 +7,15 @@
 from pathlib import Path
 import sqlite3
 from unittest import mock, IsolatedAsyncioTestCase
+import lifeblood
 from lifeblood.attribute_serialization import serialize_attributes_core, deserialize_attributes_core
 from lifeblood.enums import TaskState
 from lifeblood.db_misc import sql_init_script
-from lifeblood.basenode import BaseNode, ProcessingResult
+from lifeblood.basenode import BaseNode
+from lifeblood.nodethings import ProcessingResult
 from lifeblood.exceptions import NodeNotReadyToProcess
 from lifeblood.scheduler import Scheduler
-from lifeblood.main_scheduler import create_default_scheduler
+from lifeblood_testing_common.common import create_default_scheduler
 from lifeblood.worker import Worker
 from lifeblood.invocationjob import InvocationJob, Environment
 from lifeblood.scheduler.pinger import Pinger
@@ -25,7 +27,13 @@
 from typing import Any, Callable, Dict, List, Mapping, Optional, Set, Union
 
 
-plugin_data_provider = PluginNodeDataProvider()
+# TODO: fix PluginNodeDataProvider's multiple instance problem and remove this global usage
+plugin_data_provider = PluginNodeDataProvider(
+    plugin_paths=(
+        (Path(lifeblood.__file__).parent / 'stock_nodes', 'stock'),
+        (Path(lifeblood.__file__).parent / 'core_nodes', 'core'),
+    )
+)
 
 
 def create_node(node_type: str, node_name: str, scheduler, node_id):
@@ -89,7 +97,7 @@ def set_input_name(self, name: str):
         self.__task_dict['node_input_name'] = self.__input_name
 
     def get_context_for(self, node: BaseNode) -> ProcessingContext:
-        return ProcessingContext(node, self.task_dict())
+        return ProcessingContext(node, self.task_dict(), {})
 
     def task_dict(self) -> dict:
         return {**self.__task_dict, **{
@@ -198,7 +206,8 @@ async def _helper_test_worker_node(self,
                                        task_done_logic: Optional[Callable] = None,
                                        runcode: Optional[str] = None,
                                        worker_count: int = 1,
-                                       tasks_to_complete=None):
+                                       tasks_to_complete=None,
+                                       **kwargs):
         """
         generic logic runner helper.
         this will start scheduler and worker,
@@ -219,7 +228,7 @@ async def _helper_test_worker_node(self,
             ppatch.return_value = mock.AsyncMock(Pinger)
             wppatch.return_value = mock.AsyncMock()
 
-            sched = create_default_scheduler('test_swc.db', do_broadcasting=False, helpers_minimal_idle_to_ensure=0)
+            sched = create_default_scheduler('test_swc.db', do_broadcasting=False, helpers_minimal_idle_to_ensure=0, **kwargs)
             await sched.start()
 
             workers = []
@@ -327,7 +336,7 @@ async def _logic(scheduler, workers, tmp_script_path, done_waiter):
                             'outimage': out_exr_path,
                             'frames': [1, 2, 3]
                         }
-                        res = node.process_task(ProcessingContext(node, {'attributes': serialize_attributes_core(start_attrs)}))
+                        res = node.process_task(ProcessingContext(node, {'attributes': serialize_attributes_core(start_attrs)}, {}))
 
                         ij = res.invocation_job
                         self.assertTrue(ij is not None)
@@ -357,7 +366,7 @@ async def _logic(scheduler, workers, tmp_script_path, done_waiter):
                         res = node.postprocess_task(ProcessingContext(node, {'attributes': serialize_attributes_core({
                             **start_attrs,
                             **updated_attrs
-                        })}))
+                        })}, {}))
                         if res.attributes_to_set:
                             updated_attrs.update(res.attributes_to_set)
 
@@ -427,7 +436,7 @@ async def _logic(scheduler, workers, script_path, done_waiter):
                     for param, val in params.items():
                         node.set_param_value(param, val)
 
-                res = node.process_task(ProcessingContext(node, {'attributes': serialize_attributes_core(task_attrs)}))
+                res = node.process_task(ProcessingContext(node, {'attributes': serialize_attributes_core(task_attrs)}, {}))
                 if res.attributes_to_set:
                     updated_attrs.update(res.attributes_to_set)
 
@@ -462,7 +471,7 @@ async def _logic(scheduler, workers, script_path, done_waiter):
                 res = node.postprocess_task(ProcessingContext(node, {'attributes': serialize_attributes_core({
                     **task_attrs,
                     **updated_attrs
-                })}))
+                })}, {}))
                 if res.attributes_to_set:
                     updated_attrs.update(res.attributes_to_set)
 
diff --git a/src/lifeblood_testing_common/scheduler_config_provider_default_override.py b/src/lifeblood_testing_common/scheduler_config_provider_default_override.py
new file mode 100644
index 00000000..24b918b3
--- /dev/null
+++ b/src/lifeblood_testing_common/scheduler_config_provider_default_override.py
@@ -0,0 +1,84 @@
+from lifeblood.scheduler_config_provider_default import SchedulerConfigProviderDefaults
+from lifeblood.nethelpers import all_interfaces
+
+from typing import Any, Dict, List, Optional, Tuple
+
+from lifeblood.worker_resource_definition import WorkerResourceDefinition
+
+
+class SchedulerConfigProviderOverrides(SchedulerConfigProviderDefaults):
+    def __init__(
+            self,
+            main_db_location: Optional[str] = None,
+            main_db_connection_timeout: Optional[float] = None,
+            legacy_server_address: Optional[Tuple[str, int]] = None,
+            message_processor_address: Optional[Tuple[str, int]] = None,
+            ui_address: Optional[Tuple[str, int]] = None,
+            do_broadcast: Optional[bool] = None,
+            broadcast_interval: Optional[float] = None,
+            minimal_idle_helpers: Optional[int] = None,
+            node_per_node_config: Optional[Dict[str, Dict[str, Any]]] = None,
+            node_global_config: Optional[Dict[str, Dict[str, Any]]] = None,
+            resource_definitions: Optional[Tuple[WorkerResourceDefinition, ...]] = None,
+    ):
+        super().__init__()
+        self.__main_db_location_override = main_db_location
+        self.__main_db_connection_timeout = main_db_connection_timeout
+        self.__do_broadcast_override = do_broadcast
+        self.__broadcast_interval_override = broadcast_interval
+        self.__minimal_idle_helpers = minimal_idle_helpers
+        self.__legacy_address = legacy_server_address
+        self.__message_processor_address = message_processor_address
+        self.__ui_address = ui_address
+        self.__node_per_node_config = node_per_node_config or {}
+        self.__node_global_config = node_global_config or {}
+        self.__resource_definitions = resource_definitions
+
+    def main_database_location(self) -> str:
+        return self.__main_db_location_override or super().main_database_location()
+
+    def main_database_connection_timeout(self) -> float:
+        return self.__main_db_connection_timeout or super().main_database_connection_timeout()
+
+    def _config_do_broadcast(self) -> Optional[bool]:
+        return self.__do_broadcast_override if self.__do_broadcast_override is not None else super()._config_do_broadcast()
+
+    def _config_broadcast_interval(self) -> Optional[float]:
+        return self.__broadcast_interval_override if self.__broadcast_interval_override is not None else super()._config_broadcast_interval()
+
+    def scheduler_helpers_minimal(self) -> int:
+        return self.__minimal_idle_helpers or super().scheduler_helpers_minimal()
+
+    def legacy_server_address(self) -> Optional[Tuple[str, int]]:
+        return self.__legacy_address or super().legacy_server_address()
+
+    def _expand_catchall_address(self, address: str) -> List[str]:
+        ret = []
+        if address == '0.0.0.0':  # message_processor address must be addressable, no catchall
+            for iface_ip in all_interfaces():
+                ret.append(iface_ip)
+        else:
+            ret.append(address)
+        return ret
+
+    def server_message_addresses(self) -> Tuple[Tuple[str, int], ...]:
+        if self.__message_processor_address is None:
+            return super().server_message_addresses()
+        ret = []
+        for server_ip in self._expand_catchall_address(self.__message_processor_address[0]):
+            addr = (server_ip, self.__message_processor_address[1])
+            ret.append(addr)
+        return tuple(ret)
+
+    def server_ui_address(self) -> Tuple[str, int]:
+        return self.__ui_address or super().server_ui_address()
+
+    def node_configuration(self, node_type_id: str) -> dict:
+        return {
+            **super().node_configuration(node_type_id),
+            **self.__node_global_config,
+            **self.__node_per_node_config.get(node_type_id, {}),
+        }
+
+    def hardware_resource_definitions(self) -> Tuple[WorkerResourceDefinition, ...]:
+        return self.__resource_definitions or super().hardware_resource_definitions()
diff --git a/tests/nodes/data/test_basic_stuff.db b/tests/nodes/data/test_basic_stuff.db
new file mode 100644
index 00000000..de584fab
Binary files /dev/null and b/tests/nodes/data/test_basic_stuff.db differ
diff --git a/tests/nodes/test_attribute_splitter.py b/tests/nodes/test_attribute_splitter.py
index 6aa0623f..017e7e8d 100644
--- a/tests/nodes/test_attribute_splitter.py
+++ b/tests/nodes/test_attribute_splitter.py
@@ -1,7 +1,7 @@
 from asyncio import Event
 from lifeblood.scheduler import Scheduler
 from lifeblood.worker import Worker
-from lifeblood.basenode import ProcessingError
+from lifeblood.nodethings import ProcessingError
 from lifeblood_testing_common.nodes_common import TestCaseBase, PseudoContext
 
 from typing import List
diff --git a/tests/nodes/test_basic_features_integration.py b/tests/nodes/test_basic_features_integration.py
new file mode 100644
index 00000000..dcedf7d6
--- /dev/null
+++ b/tests/nodes/test_basic_features_integration.py
@@ -0,0 +1,47 @@
+from lifeblood_testing_common.integration_common import FullIntegrationTestCase
+
+from typing import Iterable, Tuple
+
+
+class TestNodeStuff(FullIntegrationTestCase):
+    @classmethod
+    def _initial_db_file(cls) -> str:
+        return 'data/test_basic_stuff.db'
+
+    @classmethod
+    def node_configs(cls) -> Tuple[dict, dict]:
+        return {'set_attrib': {
+            'conf1': 12,
+            'conf2': 2.345,
+        }}, {
+            'conf1': 55,
+            'conf3': 'foo be me',
+        }
+
+    async def _create_test_tasks(self) -> Iterable[int]:
+        tasks = [
+            *await self._create_task(node_name='IN EXPR CONTEXT'),
+        ]
+        return tasks
+
+    def _expected_task_attributes(self):
+        return {
+            0: {
+                'test1': 12,
+                'test2': 2.345,
+                'test3': 'foo be me',
+                'test4': '12',
+            },
+        }
+
+    async def _additional_checks_on_finish(self):
+        pass
+
+    def _minimal_idle_to_ensure(self) -> int:
+        return 0
+
+    def _minimal_total_to_ensure(self) -> int:
+        return 0
+
+    def _timeout(self) -> float:
+        return 30.0
diff --git a/tests/nodes/test_hardware_resources.py b/tests/nodes/test_hardware_resources.py
new file mode 100644
index 00000000..291105ca
--- /dev/null
+++ b/tests/nodes/test_hardware_resources.py
@@ -0,0 +1,82 @@
+from unittest import TestCase
+from lifeblood.hardware_resources import HardwareResources, HardwareResource
+
+
+class TestHardwareResources(TestCase):
+    def test_eq(self):
+        res1 = HardwareResources(
+            hwid=1357,
+            qwe=22.33
+        )
+        res1a = HardwareResources(
+            hwid=1357,
+            qwe=22.33
+        )
+        res2 = HardwareResources(
+            hwid=1357,
+            qwe=22.33,
+            asd=33,
+        )
+        res3 = HardwareResources(
+            hwid=1358,
+            qwe=22.33,
+        )
+        res4 = HardwareResources(
+            hwid=1357,
+            qwe=22.34,
+        )
+        res5 = HardwareResources(
+            hwid=1357,
+            asd=22.33,
+        )
+
+        self.assertEqual(res1, res1)
+        self.assertEqual(res1a, res1a)
+        self.assertEqual(res2, res2)
+        self.assertEqual(res3, res3)
+        self.assertEqual(res4, res4)
+        self.assertEqual(res5, res5)
+
+        self.assertEqual(res1, res1a)
+        self.assertEqual(res1a, res1)
+        self.assertNotEqual(res1, res2)
+        self.assertNotEqual(res1, res3)
+        self.assertNotEqual(res1, res4)
+        self.assertNotEqual(res1, res5)
+        self.assertNotEqual(res2, res1)
+        self.assertNotEqual(res3, res1)
+        self.assertNotEqual(res4, res1)
+        self.assertNotEqual(res5, res1)
+
+    def test_serde(self):
+        res = HardwareResources(
+            hwid=135246357,
+            foo_bunny=55.6,
+            bar_chicken=23,
+        )
+
+        act = HardwareResources.deserialize(res.serialize())
+
+        self.assertEqual(res, act)
+
+    def test_get_items(self):
+        res = HardwareResources(
+            hwid=135246357,
+            foo_bunny=55.6,
+            bar_chicken=23,
+            car_cdr=1234567890123456
+        )
+
+        self.assertIsInstance(res['foo_bunny'], HardwareResource)
+        self.assertIsInstance(res['bar_chicken'], HardwareResource)
+        self.assertIsInstance(res['car_cdr'], HardwareResource)
+
+        self.assertEqual(res.hwid, 135246357)
+        self.assertEqual(res['foo_bunny'].value, 55.6)
+        self.assertEqual(res['bar_chicken'].value, 23)
+        self.assertEqual(res['car_cdr'].value, 1234567890123456)
+
+        self.assertIn('foo_bunny', res)
+        self.assertIn('bar_chicken', res)
+        self.assertIn('car_cdr', res)
+        self.assertNotIn('door_lore', res)
diff --git a/tests/nodes/test_wedge.py b/tests/nodes/test_wedge.py
index 96840021..21b9ffb8 100644
--- a/tests/nodes/test_wedge.py
+++ b/tests/nodes/test_wedge.py
@@ -2,7 +2,7 @@
 from asyncio import Event
 from lifeblood.scheduler import Scheduler
 from lifeblood.worker import Worker
-from lifeblood.basenode import ProcessingError
+from lifeblood.nodethings import ProcessingError
 from lifeblood_testing_common.nodes_common import TestCaseBase, PseudoContext
 
 from typing import List
diff --git a/tests/test_basenode.py b/tests/test_basenode.py
index 5c500259..d2717e38 100644
--- a/tests/test_basenode.py
+++ b/tests/test_basenode.py
@@ -1,11 +1,15 @@
+import os.path
 import random
-from unittest import TestCase, mock
 import sqlite3
-from lifeblood.basenode import BaseNodeWithTaskRequirements, BaseNode, ProcessingResult
+from unittest import IsolatedAsyncioTestCase
+import tempfile
+from lifeblood.node_plugin_base import BaseNodeWithTaskRequirements, ProcessingResult
+from lifeblood.worker_resource_definition import WorkerResourceDefinition, WorkerResourceDataType
 from lifeblood.processingcontext import ProcessingContext
 from lifeblood.invocationjob import InvocationJob
 from lifeblood.enums import WorkerType
-from lifeblood.db_misc import sql_init_script
+from lifeblood.scheduler.data_access import DataAccess
+from lifeblood_testing_common.scheduler_config_provider_default_override import SchedulerConfigProviderOverrides
 
 
 class NoNodeWithReq(BaseNodeWithTaskRequirements):
@@ -22,36 +26,47 @@ def process_task(self, context: ProcessingContext) -> ProcessingResult:
         return ProcessingResult(ij)
 
 
-class TestBaseNodes(TestCase):
-    def test_requirements(self):
+class TestBaseNodes(IsolatedAsyncioTestCase):
+    async def test_requirements(self):
         node = NoNodeWithReq('foo')
-        node.set_param_value('worker cpu cost', 1.2)
-        node.set_param_value('worker cpu cost preferred', 2.3)
-        node.set_param_value('worker gpu cost', 3.4)
-        node.set_param_value('worker gpu cost preferred', 4.5)
-        node.set_param_value('worker mem cost', 5.6)
-        node.set_param_value('worker mem cost preferred', 6.7)
-        node.set_param_value('worker gpu mem cost', 7.8)
-        node.set_param_value('worker gpu mem cost preferred', 8.9)
 
-        node.set_param_value('worker groups', 'sasha masha dasha')
-        node.set_param_value('worker type', WorkerType.SCHEDULER_HELPER.value)
-        node.set_param_value('priority adjustment', 9.0)
-
-        res = node._process_task_wrapper({})
+        node.set_param_value('__requirements__.res', 4)
+        node.set_param_value('__requirements__.name_res_0', "cpu_count")
+        node.set_param_value('__requirements__.type_res_0', 0)  # float
+        node.set_param_value('__requirements__.f_min_res_0', 1.2)
+        node.set_param_value('__requirements__.f_pref_res_0', 2.3)
+        node.set_param_value('__requirements__.name_res_1', "cpu_mem_b")
+        node.set_param_value('__requirements__.type_res_1', 0)  # float
+        node.set_param_value('__requirements__.f_min_res_1', 5600000000)
+        node.set_param_value('__requirements__.f_pref_res_1', 6700000000)
+        node.set_param_value('__requirements__.name_res_2', "gpu_count")
+        node.set_param_value('__requirements__.type_res_2', 0)  # float
+        node.set_param_value('__requirements__.f_min_res_2', 3.4)
+        node.set_param_value('__requirements__.f_pref_res_2', 4.5)
+        node.set_param_value('__requirements__.name_res_3', "gpu_mem_b")
+        node.set_param_value('__requirements__.type_res_3', 0)  # float
+        node.set_param_value('__requirements__.f_min_res_3', 7800000000)
+        node.set_param_value('__requirements__.f_pref_res_3', 8900000000)
+
+        node.set_param_value('__requirements__.worker_groups', 'sasha masha dasha')
+        node.set_param_value('__requirements__.worker_type', WorkerType.SCHEDULER_HELPER.value)
+        node.set_param_value('__requirements__.priority_adjustment', 9.0)
+
+        res = node._process_task_wrapper({}, {})
 
         reqs = res.invocation_job.requirements()
+        print(reqs.final_where_clause())
 
         # test individual elements
 
-        self.assertEqual(1.2, reqs.min_cpu_count())
-        self.assertEqual(2.3, reqs.preferred_cpu_count())
-        self.assertEqual(3.4, reqs.min_gpu_count())
-        self.assertEqual(4.5, reqs.preferred_gpu_count())
-        self.assertEqual(5600000000, reqs.min_memory_bytes())
-        self.assertEqual(6700000000, reqs.preferred_memory_bytes())
-        self.assertEqual(7800000000, reqs.min_gpu_memory_bytes())
-        self.assertEqual(8900000000, reqs.preferred_gpu_memory_bytes())
+        self.assertEqual(1.2, reqs.min_resource('cpu_count'))  # .min_cpu_count())
+        self.assertEqual(2.3, reqs.preferred_resource('cpu_count'))  # .preferred_cpu_count())
+        self.assertEqual(3.4, reqs.min_resource('gpu_count'))  # .min_gpu_count())
+        self.assertEqual(4.5, reqs.preferred_resource('gpu_count'))  # .preferred_gpu_count())
+        self.assertEqual(5600000000, reqs.min_resource('cpu_mem_b'))  # .min_memory_bytes())
+        self.assertEqual(6700000000, reqs.preferred_resource('cpu_mem_b'))  # .preferred_memory_bytes())
+        self.assertEqual(7800000000, reqs.min_resource('gpu_mem_b'))  # .min_gpu_memory_bytes())
+        self.assertEqual(8900000000, reqs.preferred_resource('gpu_mem_b'))  # .preferred_gpu_memory_bytes())
 
         self.assertSetEqual({'sasha', 'masha', 'dasha'}, reqs.groups())
         self.assertEqual(WorkerType.SCHEDULER_HELPER, reqs.worker_type())
@@ -62,76 +77,118 @@ def test_requirements(self):
         sql_part = reqs.final_where_clause()
         print(sql_part)
         self.assertIn('cpu_count', sql_part)
-        self.assertIn('cpu_mem', sql_part)
+        self.assertIn('cpu_mem_b', sql_part)
         self.assertIn('gpu_count', sql_part)
-        self.assertIn('gpu_mem', sql_part)
+        self.assertIn('gpu_mem_b', sql_part)
 
         rng = random.Random(1366613)
         _stat_pass = 0
         _stat_fail = 0
-        for i in range(1000):
-            with sqlite3.connect(':memory:') as con:
-                con.executescript(sql_init_script)
-                con.commit()
-                cur = con.cursor()
-                cur.execute('SELECT "id" FROM "workers"')
-                self.assertEqual(0, len(cur.fetchall()))
-
-                cpu_c = rng.uniform(1, 4)
-                gpu_c = rng.uniform(3, 6)
-                cpu_m = rng.randint(5000000000, 8000000000)
-                gpu_m = rng.randint(7000000000, 10000000000)
-                wt = rng.choice(list(WorkerType))
-
-                con.execute('INSERT INTO "workers" '
-                            '(hwid, '
-                            'last_address, last_seen, ping_state, state, worker_type) '
-                            'VALUES '
-                            '(?, ?, ?, ?, ?, ?)',
-                            (12345, '', 0, 0, 0, wt.value))
-                con.execute('INSERT INTO resources '
-                            '(hwid, cpu_count, total_cpu_count, '
-                            'cpu_mem, total_cpu_mem, '
-                            'gpu_count, total_gpu_count, '
-                            'gpu_mem, total_gpu_mem) '
-                            'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) '
-                            'ON CONFLICT(hwid) DO UPDATE SET '
-                            'cpu_count=excluded.cpu_count, total_cpu_count=excluded.total_cpu_count, '
-                            'cpu_mem=excluded.cpu_mem, total_cpu_mem=excluded.total_cpu_mem, '
-                            'gpu_count=excluded.gpu_count, total_gpu_count=excluded.total_gpu_count, '
-                            'gpu_mem=excluded.gpu_mem, total_gpu_mem=excluded.total_gpu_mem',
-                            (12345,
-                             cpu_c,
-                             cpu_c,
-                             cpu_m,
-                             cpu_m,
-                             gpu_c,
-                             gpu_c,
-                             gpu_m,
-                             gpu_m)
-                            )
-
-                grps = rng.sample(('sasha', 'masha', 'dasha', 'pasha', 'lesha'), rng.randint(0, 3))
-                if grps:
-                    con.executemany(f'INSERT INTO worker_groups (worker_hwid, "group") VALUES (?, ?)',
-                                    ((12345, x) for x in grps))
-                con.commit()
-
-                cur.execute(f'SELECT "id", "cpu_count", "cpu_mem", "gpu_count", "gpu_mem" FROM workers '
-                            f'LEFT JOIN resources ON workers.hwid == resources.hwid '
-                            f'WHERE {sql_part}')
-                res = cur.fetchall()
-                if all((1.2 <= cpu_c,
-                        5600000000 <= cpu_m,
-                        3.4 <= gpu_c,
-                        7800000000 <= gpu_m,
-                        WorkerType.SCHEDULER_HELPER == wt,
-                        'sasha' in grps or 'masha' in grps or 'dasha' in grps)):
-                    self.assertEqual(1, len(res), f'{res} {(cpu_c, cpu_m, gpu_c, gpu_m)}')
-                    self.assertEqual(1, res[0][0], f'{res} {(cpu_c, cpu_m, gpu_c, gpu_m)}')
-                    _stat_pass += 1
-                else:
-                    self.assertEqual(0, len(res), f'{res} {(cpu_c, cpu_m, gpu_c, gpu_m)}')
-                    _stat_fail += 1
 
+        fd, temp_db_path = tempfile.mkstemp(suffix='_test.db', dir='/dev/shm' if os.path.exists('/dev/shm') else None)
+        try:
+            for i in range(100):
+                with open(temp_db_path, 'w') as f:
+                    pass
+                data_access = DataAccess(config_provider=SchedulerConfigProviderOverrides(
+                    temp_db_path, 30,
+                    resource_definitions=(  # this matches what we defined in params above, would be nice to combine these
+                        WorkerResourceDefinition('cpu_count', WorkerResourceDataType.GENERIC_FLOAT, '', ''),
+                        WorkerResourceDefinition('cpu_mem_b', WorkerResourceDataType.GENERIC_FLOAT, '', ''),
+                        WorkerResourceDefinition('gpu_count', WorkerResourceDataType.GENERIC_FLOAT, '', ''),
+                        WorkerResourceDefinition('gpu_mem_b', WorkerResourceDataType.GENERIC_FLOAT, '', ''),
+                    ),
+                ))
+                async with data_access.data_connection() as con:
+                    async with con.execute('SELECT "id" FROM "workers"') as cur:
+                        self.assertEqual(0, len(await cur.fetchall()))
+
+                    cpu_c = rng.uniform(1, 4)
+                    gpu_c = rng.uniform(3, 6)
+                    cpu_m = rng.randint(5000000000, 8000000000)
+                    gpu_m = rng.randint(7000000000, 10000000000)
+                    wt = rng.choice(list(WorkerType))
+
+                    await con.execute('INSERT INTO "workers" '
+                                      '(hwid, '
+                                      'last_address, last_seen, ping_state, state, worker_type) '
+                                      'VALUES '
+                                      '(?, ?, ?, ?, ?, ?)',
+                                      (12345, '', 0, 0, 0, wt.value))
+                    await con.execute('INSERT INTO resources '
+                                      '(hwid, cpu_count, total_cpu_count, '
+                                      'cpu_mem_b, total_cpu_mem_b, '
+                                      'gpu_count, total_gpu_count, '
+                                      'gpu_mem_b, total_gpu_mem_b) '
+                                      'VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) '
+                                      'ON CONFLICT(hwid) DO UPDATE SET '
+                                      'cpu_count=excluded.cpu_count, total_cpu_count=excluded.total_cpu_count, '
+                                      'cpu_mem_b=excluded.cpu_mem_b, total_cpu_mem_b=excluded.total_cpu_mem_b, '
+                                      'gpu_count=excluded.gpu_count, total_gpu_count=excluded.total_gpu_count, '
+                                      'gpu_mem_b=excluded.gpu_mem_b, total_gpu_mem_b=excluded.total_gpu_mem_b',
+                                      (12345,
+                                       cpu_c,
+                                       cpu_c,
+                                       cpu_m,
+                                       cpu_m,
+                                       gpu_c,
+                                       gpu_c,
+                                       gpu_m,
+                                       gpu_m)
+                                      )
+
+                    grps = rng.sample(('sasha', 'masha', 'dasha', 'pasha', 'lesha'), rng.randint(0, 3))
+                    if grps:
+                        await con.executemany(f'INSERT INTO worker_groups (worker_hwid, "group") VALUES (?, ?)',
+                                              ((12345, x) for x in grps))
+                    await con.commit()
+
+                    async with con.execute(f'SELECT "id", "cpu_count", "cpu_mem_b", "gpu_count_b", "gpu_mem" FROM workers '
+                                           f'LEFT JOIN resources ON workers.hwid == resources.hwid '
+                                           f'WHERE {sql_part}') as cur:
+                        res = await cur.fetchall()
+                    if all((1.2 <= cpu_c,
+                            5600000000 <= cpu_m,
+                            3.4 <= gpu_c,
+                            7800000000 <= gpu_m,
+                            WorkerType.SCHEDULER_HELPER == wt,
+                            'sasha' in grps or 'masha' in grps or 'dasha' in grps)):
+                        self.assertEqual(1, len(res), f'{res} {(cpu_c, cpu_m, gpu_c, gpu_m)}')
+                        self.assertEqual(1, res[0][0], f'{res} {(cpu_c, cpu_m, gpu_c, gpu_m)}')
+                        _stat_pass += 1
+                    else:
+                        self.assertEqual(0, len(res), f'{res} {(cpu_c, cpu_m, gpu_c, gpu_m)}')
+                        _stat_fail += 1
+        finally:
+            os.close(fd)
+            os.unlink(temp_db_path)
         print(f'wedging tests:\n\taccept: {_stat_pass}\n\tdecline: {_stat_fail}')
+
+    async def test_resource_definition_simple(self):
+        fd, temp_db_path = tempfile.mkstemp(suffix='_test.db', dir='/dev/shm' if os.path.exists('/dev/shm') else None)
+        try:
+            config = SchedulerConfigProviderOverrides(
+                main_db_location=temp_db_path,
+                resource_definitions=(
+                    WorkerResourceDefinition('fooo', WorkerResourceDataType.GENERIC_FLOAT, '', '', 12.3),
+                    WorkerResourceDefinition('boar', WorkerResourceDataType.GENERIC_INT, '', '', 234),
+                )
+            )
+
+            data_access = DataAccess(config_provider=config)
+
+            # Not the best test, as it tests with implementation-specific defaults
+            # but before data_access is properly separated from scheduler - there's no proper unit testing it separately
+            async with data_access.data_connection() as con:
+                con.row_factory = sqlite3.Row
+                async with con.execute('PRAGMA table_info(resources)') as cur:
+                    resource_rows = {x['name']: x for x in await cur.fetchall() if x['name'] != 'hwid'}
+
+            self.assertSetEqual({'fooo', 'total_fooo', 'boar', 'total_boar'}, set(resource_rows.keys()))
+            self.assertEqual(12.3, float(resource_rows['fooo']['dflt_value']))
+            self.assertEqual(12.3, float(resource_rows['total_fooo']['dflt_value']))
+            self.assertEqual(234, int(resource_rows['boar']['dflt_value']))
+            self.assertEqual(234, int(resource_rows['total_boar']['dflt_value']))
+        finally:
+            os.close(fd)
+            os.unlink(temp_db_path)
diff --git a/tests/test_config.py b/tests/test_config.py
index 8fa52e10..01a3bb36 100644
--- a/tests/test_config.py
+++ b/tests/test_config.py
@@ -3,7 +3,7 @@
 import shutil
 import toml  # for raw file comparison
 from lifeblood.config import get_config
-
+from lifeblood.scheduler_config_provider_file import SchedulerConfigProviderFile
 
 class StandardConfigTest(unittest.TestCase):
     _stash = None
@@ -96,11 +96,13 @@ def test_condigd(self):
 
 class DefaultComponentConfigTest(unittest.TestCase):
     def test_default_schediler(self):
-        from lifeblood.main_scheduler import default_config
-        data = toml.loads(default_config)
-        self.assertIn('scheduler', data)
-        self.assertIn('globals', data['scheduler'])
-        self.assertNotEqual('', data['scheduler']['globals']['global_scratch_location'])
+        config_text = SchedulerConfigProviderFile.generate_default_config_text()
+        data = toml.loads(config_text)
+        # not much we can test just like that,
+        # TODO: ideally would be to test for existence of certain form of commented entries, values of which bein equal to defaults.
+        self.assertIn('nodes', data)
+        self.assertIn('globals', data['nodes'])
+        self.assertNotEqual('', data['nodes']['globals']['global_scratch_location'])
 
     def test_default_worker(self):
         """
diff --git a/tests/test_scheduler.py b/tests/test_scheduler.py
index 3e2dccc1..a9e05c3a 100644
--- a/tests/test_scheduler.py
+++ b/tests/test_scheduler.py
@@ -12,7 +12,7 @@
 from lifeblood.net_messages.impl.tcp_simple_command_message_processor import TcpJsonMessageProcessor
 from lifeblood.net_messages.exceptions import MessageTransferError
 from lifeblood.scheduler_task_protocol import SchedulerTaskClient
-from lifeblood.main_scheduler import create_default_scheduler
+from lifeblood_testing_common.common import create_default_scheduler
 
 
 def purge_db(recreate=True):
diff --git a/tests/test_scheduler_worker_comm.py b/tests/test_scheduler_worker_comm.py
index a7dab68d..94e4b943 100644
--- a/tests/test_scheduler_worker_comm.py
+++ b/tests/test_scheduler_worker_comm.py
@@ -3,25 +3,18 @@
 from unittest import IsolatedAsyncioTestCase, mock
 from pathlib import Path
 import asyncio
-import random
-import signal
-import subprocess
 import time
 import sqlite3
 import logging
 
-from lifeblood.logging import get_logger
 from lifeblood.worker import Worker
 from lifeblood.invocationjob import InvocationJob
-from lifeblood.scheduler import Scheduler
 from lifeblood.taskspawn import NewTask
 from lifeblood.enums import WorkerType, WorkerState, SpawnStatus, InvocationState, InvocationMessageResult
 from lifeblood.db_misc import sql_init_script
 from lifeblood.logging import set_default_loglevel
-from lifeblood.config import get_config
-from lifeblood.nethelpers import get_default_addr
 from lifeblood.net_messages.address import AddressChain
-from lifeblood.main_scheduler import create_default_scheduler
+from lifeblood_testing_common.common import create_default_scheduler
 
 from typing import Awaitable, Callable, List, Optional, Tuple
 
diff --git a/tests/test_serializationv2.py b/tests/test_serializationv2.py
index 7992a7d1..9c6daca8 100644
--- a/tests/test_serializationv2.py
+++ b/tests/test_serializationv2.py
@@ -145,6 +145,7 @@ def test_simple(self):
 
         test1_data, test1_state = ser.serialize(test1)
 
-        test1_act = ser.deserialize(parent, 123, dataprov, test1_data, test1_state)
+        test1_act = ser.deserialize(dataprov, test1_data, test1_state)
+        test1_act.set_parent(parent, 123)
 
         self.nodes_are_same(test1, test1_act)
diff --git a/tests/test_spawn_tasks_race.py b/tests/test_spawn_tasks_race.py
index 7cf0bce9..501ed172 100644
--- a/tests/test_spawn_tasks_race.py
+++ b/tests/test_spawn_tasks_race.py
@@ -1,5 +1,6 @@
 import asyncio
 from lifeblood_testing_common.integration_common import IsolatedAsyncioTestCaseWithDb
+from lifeblood_testing_common.scheduler_config_provider_default_override import SchedulerConfigProviderOverrides
 from lifeblood_testing_common.common import chain
 from lifeblood.scheduler.scheduler import Scheduler
 from lifeblood.taskspawn import NewTask
@@ -12,7 +13,8 @@ async def test_race(self):
         which in reality can happen for example in _awaiter (where transaction is passed to spawn),
         and spawn called by message server for example.
         """
-        sched = Scheduler(self.db_file, do_broadcasting=False, node_data_provider=None, node_serializers=[None])
+        config = SchedulerConfigProviderOverrides(main_db_location=self.db_file, do_broadcast=False)
+        sched = Scheduler(scheduler_config_provider=config, node_data_provider=None, node_serializers=[None])
         async with sched.data_access.data_connection() as con:
             await con.execute('BEGIN IMMEDIATE')
             task1 = asyncio.create_task(sched.spawn_tasks([NewTask('foo1', 1, None, {})]))
diff --git a/tests/test_task_blocking.py b/tests/test_task_blocking.py
index b0354a02..4efa5b70 100644
--- a/tests/test_task_blocking.py
+++ b/tests/test_task_blocking.py
@@ -2,6 +2,7 @@
 from unittest import IsolatedAsyncioTestCase
 from lifeblood.scheduler.data_access import DataAccess, TaskSpawnData
 from lifeblood.enums import TaskState
+from lifeblood_testing_common.scheduler_config_provider_default_override import SchedulerConfigProviderOverrides
 
 
 class TestTaskBlocking(IsolatedAsyncioTestCase):
@@ -14,7 +15,7 @@ def setUp(self):
             os.unlink(self.db_file)
 
     async def test_simple(self):
-        data_access = DataAccess(self.db_file, 60)
+        data_access = DataAccess(config_provider=SchedulerConfigProviderOverrides(self.db_file, 60))
         node_id = await data_access.create_node('null', 'test node')
         task_id = await data_access.create_task(TaskSpawnData('test task', None, {}, TaskState.WAITING, node_id, 'main', None))
 
@@ -60,7 +61,7 @@ async def test_simple(self):
         self.assertTrue(await data_access.is_task_blocked(task_id))
 
     async def test_reset(self):
-        data_access = DataAccess(self.db_file, 60)
+        data_access = DataAccess(config_provider=SchedulerConfigProviderOverrides(self.db_file, 60))
         node_id = await data_access.create_node('null', 'test node')
         task_id = await data_access.create_task(TaskSpawnData('test task', None, {}, TaskState.WAITING, node_id, 'main', None))
 
diff --git a/tests/test_ui_protocol_data.py b/tests/test_ui_protocol_data.py
index 78f18613..9ec75cc8 100644
--- a/tests/test_ui_protocol_data.py
+++ b/tests/test_ui_protocol_data.py
@@ -5,8 +5,8 @@
 from lifeblood.buffered_connection import BufferedReaderWrapper
 from lifeblood.worker_metadata import WorkerMetadata
 from lifeblood.ui_protocol_data import UiData, NodeGraphStructureData, \
-    NodeData, NodeConnectionData, TaskBatchData, TaskData, WorkerData, WorkerResources, WorkerBatchData, \
-    TaskGroupData, TaskGroupStatisticsData, TaskGroupBatchData, TaskDelta
+    NodeData, NodeConnectionData, TaskBatchData, TaskData, WorkerData, WorkerResources, WorkerResource, WorkerResourceType, \
+    WorkerBatchData, TaskGroupData, TaskGroupStatisticsData, TaskGroupBatchData, TaskDelta
 from lifeblood.ui_events import TasksChanged, TasksUpdated, TasksRemoved, TaskFullState, TaskEvent
 
 
@@ -25,7 +25,14 @@ def test_trivial1(self):
                 11: TaskData(11, None, 10, 5, TaskState.ERROR, "oh noooo", True, 1531, "main", "out",
                              'a task', 2, 11, 52.1, 8484, 158, 816, {'groo', 'froo'})
             }), WorkerBatchData(0, {
-                121: WorkerData(121, WorkerResources(12.3, 23.4, 929283838, 939384848, 56.7, 67.8, 84847575, 85857676),
+                121: WorkerData(121, WorkerResources(
+                    [
+                        WorkerResource(12.3, 23.4, WorkerResourceType.FLOAT, 'cpu_count'),
+                        WorkerResource(929283838, 939384848, WorkerResourceType.INT, 'cpu_mem'),
+                        WorkerResource(56.7, 67.8, WorkerResourceType.FLOAT, 'gpu_count'),
+                        WorkerResource(84847575, 85857676, WorkerResourceType.INT, 'gpu_mem'),
+                    ]
+                ),
                                 '1928374', '127.1.2.333:blo', 1234567, WorkerState.BUSY, WorkerType.STANDARD,
                                 1534, 8273, 7573, 55.5, {"borker", "gorker"},
                                 WorkerMetadata("fooo"))
diff --git a/tests/test_ui_state_accessor_integration.py b/tests/test_ui_state_accessor_integration.py
index 8517bc81..13d886c7 100644
--- a/tests/test_ui_state_accessor_integration.py
+++ b/tests/test_ui_state_accessor_integration.py
@@ -12,7 +12,7 @@
 from lifeblood.environment_resolver import EnvironmentResolverArguments
 from lifeblood.shared_lazy_sqlite_connection import SharedLazyAiosqliteConnection
 from lifeblood.logging import get_logger
-from lifeblood.main_scheduler import create_default_scheduler
+from lifeblood_testing_common.common import create_default_scheduler
 
 
 def purge_db(testdbpath):
diff --git a/tests/test_worker.py b/tests/test_worker.py
index 40922169..4273577b 100644
--- a/tests/test_worker.py
+++ b/tests/test_worker.py
@@ -9,7 +9,7 @@
 from lifeblood.invocationjob import InvocationJob, InvocationEnvironment
 from lifeblood.environment_resolver import EnvironmentResolverArguments
 from lifeblood.net_messages.address import AddressChain
-from lifeblood.main_scheduler import create_default_scheduler
+from lifeblood_testing_common.common import create_default_scheduler
 
 
 class RunningSchedulerTests(IsolatedAsyncioTestCase):
diff --git a/tests/test_worker_restart_double_invocation_edge_case.py b/tests/test_worker_restart_double_invocation_edge_case.py
index 8c815d7a..832b59d7 100644
--- a/tests/test_worker_restart_double_invocation_edge_case.py
+++ b/tests/test_worker_restart_double_invocation_edge_case.py
@@ -4,6 +4,7 @@
 from contextlib import contextmanager
 from lifeblood_testing_common.integration_common import IsolatedAsyncioTestCaseWithDb
 from lifeblood_testing_common.common import chain
+from lifeblood_testing_common.scheduler_config_provider_default_override import SchedulerConfigProviderOverrides
 from unittest import mock
 from lifeblood.enums import TaskState, WorkerState, WorkerPingState, TaskScheduleStatus, InvocationState
 from lifeblood.invocationjob import InvocationJob
@@ -124,8 +125,9 @@ async def test_multi_invoc3_empty1_delays2(self):
         await self._helper_test_multi_invoc(3, 1, delays=[0, 0.25, 0])
 
     async def _helper_test_multi_invoc(self, racing_tasks_count: int, num_empty_invocs: int = 0, delays: Optional[List[int]] = None):
-        sched = Scheduler(self.db_file, do_broadcasting=False, node_data_provider=None, node_serializers=[None])
-        data_access = DataAccess(self.db_file, 60)
+        config = SchedulerConfigProviderOverrides(self.db_file, 60, do_broadcast=False)
+        sched = Scheduler(scheduler_config_provider=config, node_data_provider=None, node_serializers=[None])
+        data_access = DataAccess(config_provider=config)
         m = mock.MagicMock()
         m.data_access = data_access
         task_ids = []