Repository: zefhub/zef (language: python)

Issue:
Fix thread issues, windows build, macos build into dev



Discussion / hints:

Fix patch:
diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml
index 9bb7ac80..952b77c4 100644
--- a/.github/actions/build/action.yml
+++ b/.github/actions/build/action.yml
@@ -23,10 +23,16 @@ runs:
   using: 'composite'
   steps:
 
+    - name: Setup compiler on windows
+      if: ${{ startsWith(inputs.os, 'windows') }}
+      uses: microsoft/setup-msbuild@v1.1
+      with:
+        vs-version: '17'
+
     ########
     # Python
 
-    - name: Setup python on linux
+    - name: Setup python on linux and windows
       if: ${{ !startsWith(inputs.os, 'macos') }}
       uses: actions/setup-python@v2
       with:
@@ -109,7 +115,11 @@ runs:
       if: ${{ startsWith(inputs.os, 'macos') }}
       shell: bash
       run: |
-        brew install ninja openssl zstd jq coreutils
+        brew install ninja openssl@1.1 zstd jq coreutils
+        echo
+        brew --prefix
+        echo
+        brew --prefix openssl@1.1
 
     # Only do the make if the C++ files and their deps have changed. These deps
     # are everything inside of core, but also things like the tokens files from
@@ -174,8 +184,6 @@ runs:
         ZEFHUB_AUTH_KEY: GUEST
       run: |
         export PYTHONPATH="${GITHUB_WORKSPACE}/python:$PYTHONPATH"
-        echo $PYTHONPATH
-        echo should have echoed python path
         python3 python/tests/quick_start.py
         python3 -mpytest python/tests
 
@@ -187,8 +195,8 @@ runs:
         ZEFHUB_AUTH_KEY: GUEST
       run: |
         set "PYTHONPATH=%GITHUB_WORKSPACE%/python;%PYTHONPATH%"
-        echo %PYTHONPATH%
-        echo should have echoed python path
         copy python\pyzef\build\Release\pyzef.* python\zef
         python3 python/tests/quick_start.py
-        python3 -mpytest python/tests
\ No newline at end of file
+        # Because of some strange weirdness, going to try unittest here instead of pytest
+        #python3 -mpytest python/tests
+        python3 -munittest discover -s python/tests
\ No newline at end of file
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index f9d2ff3d..d0b2010b 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -31,7 +31,8 @@ jobs:
       # TODO: Update this to check via building the wheel rather than a local build
       - name: Build
         uses: ./.github/actions/build
-        timeout-minutes: 15
+        # Windows takes forever to build so this needs to be >20min
+        timeout-minutes: 30
         with:
           python-version: ${{ matrix.python-version }}
           os: ${{ matrix.os }}
@@ -41,7 +42,7 @@ jobs:
     needs: test-code
     runs-on: ubuntu-20.04
     steps:
-      dummy:
+      - name: dummy
         run: |
           echo Nothing to see here
-        
\ No newline at end of file
+        
diff --git a/.github/workflows/pyzef-pypi-alpha.yml b/.github/workflows/pyzef-pypi-alpha.yml
index 69afeeb8..444e6beb 100644
--- a/.github/workflows/pyzef-pypi-alpha.yml
+++ b/.github/workflows/pyzef-pypi-alpha.yml
@@ -124,9 +124,11 @@ jobs:
       - name: Release announcement for dev
         env:
           MATTERMOST_URL: ${{ secrets.MATTERMOST_URL }}
-          CHANNEL: dev-releases
-          NAME: "${{ github.event.release.name }}"
-          DESCRIPTION: "${{ github.event.release.body }}"
+          CHANNEL: "dev-releases"
+          NAME: ${{ steps.vars.outputs.tag }}
+          DESCRIPTION_FILE: "release.body"
           VERSION_STRING: ${{ steps.vars.outputs.tag }}
           DOWNLOAD_STRING: "`pip install zef==${{ steps.vars.outputs.tag }}`"
-        run: go run ${GITHUB_WORKSPACE}/.github/scripts/publish_release_announcement.go
+        run: |
+          echo "Alpha/dev release of PyPI wheels for version ${VERSION_STRING}" > release.body
+          go run ${GITHUB_WORKSPACE}/.github/scripts/publish_release_announcement.go
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e26b608d..2a23aa3b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,21 @@
+pyzef-0.15.7
+============
+
+Breaking changes:
+
+- SimpleGQL: no longer takes the schema_file and graph as regular arguments, but
+  as `--schema-file` and `--data-tag` instead.
+  
+New features:
+
+- SimpleGQL:
+    - all main module arguments can be passed as environment variables,
+    e.g. instead of `--schema-file` you can use `SIMPLEGQL_SCHEMA_FILE`.
+    Commandline args take priority.
+    - `--init-hook` option to run a hook on every startup
+    - `@dynamic(hook: "...")` option for field resolvers
+
+
 pyzef-0.15.6
 ============
 
diff --git a/compile_for_local_dev.sh b/compile_for_local_dev.sh
index bd4ab4ea..be4564f5 100755
--- a/compile_for_local_dev.sh
+++ b/compile_for_local_dev.sh
@@ -53,7 +53,15 @@ python3 -m pip install -qr python/requirements.txt || exit 1
     [ -d build ] || mkdir build || exit 1
     cd build
 
-    if which ninja > /dev/null ; then
+    # Remove the token files if they are there to ensure we get the latest - ignoring any errors
+    rm zef-build/*.json >/dev/null 2>&1
+
+    if [ -n "$CMAKE_GENERATOR" ] ; then
+        CMAKE_ARGS="-j $np"
+    elif [[ "$OSTYPE" == "msys" || "$OSTYPE" == "cygwin" ]] ; then
+        # Leave the default for CMAKE_GENERATOR so that appropriate MSVC is selected
+        CMAKE_ARGS="-j $np"
+    elif which ninja > /dev/null ; then
         export CMAKE_GENERATOR=Ninja
         CMAKE_ARGS=""
     else
@@ -67,9 +75,10 @@ python3 -m pip install -qr python/requirements.txt || exit 1
         CMAKE_ARGS="-j $np"
     fi
     # The -DPython3_EXECUTABLE seems necessary here for github CI
-    cmake .. -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-Release} -DCMAKE_PREFIX_PATH=$(realpath ../../../core) -DPython3_EXECUTABLE=$(which python3) -DLIBZEF_STATIC=TRUE || exit 1
+    export CMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-Release}
+    cmake .. -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE} -DCMAKE_PREFIX_PATH=$(realpath ../../../core) -DPython3_EXECUTABLE=$(which python3) -DLIBZEF_STATIC=TRUE -DLIBZEF_FORCE_ASSERTS=TRUE || exit 1
 
-    cmake --build . --config Release $CMAKE_ARGS || exit 1
+    cmake --build . --config ${CMAKE_BUILD_TYPE} $CMAKE_ARGS || exit 1
 ) || exit 1
 
 ln -fs $(realpath python/pyzef/build/pyzef.* --relative-to=python/zef) python/zef/
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
index 6e058e73..992adc3e 100644
--- a/core/CMakeLists.txt
+++ b/core/CMakeLists.txt
@@ -22,17 +22,11 @@ project(ZefCore CXX)
 #########################
 # customizable variables
 
-if(CMAKE_BUILD_TYPE STREQUAL "Debug")
-  if(NOT WIN32)
-    set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -rdynamic -ggdb -g")
-    add_definitions(-DDEBUG)
-  endif()
-endif()
-
 option(LOCAL_ZEFTYPES "Disable getting zeftypes from zefhub" FALSE)
 option(ZEFDB_ALLOW_NO_TLS "Allow insecure websockets for ZefHub connections" TRUE)
 option(ZEFDB_TEST_NO_MMAP_CHECKS "Test for mmap slowness" FALSE)
 option(LIBZEF_PYZEF_BUNDLED "libzef is going to be bundled together with pyzef, which means putting files in different places." FALSE)
+option(LIBZEF_FORCE_ASSERTS "Make asserts happen regardless of debug/release build" FALSE)
 
 # When bundling for pyzef we default to bundling everything else.
 cmake_dependent_option(LIBZEF_STATIC "Compile as a static library rather than a dynamically linked library" FALSE "NOT LIBZEF_PYZEF_BUNDLED" TRUE)
@@ -78,6 +72,7 @@ set(SOURCES
     src/zefref.cpp
     src/uids.cpp
     src/from_json.cpp
+    src/zef_config.cpp
 
     src/butler/butler.cpp
     src/butler/butler_zefhub.cpp
@@ -140,6 +135,7 @@ if(CMAKE_CXX_COMPILER_VERSION VERSION_LESS 9)
 endif()
 
 target_link_libraries(zef PUBLIC nlohmann_json::nlohmann_json)
+target_link_libraries(zef PRIVATE yaml-cpp::yaml-cpp)
 target_link_libraries(zef PUBLIC phmap)
 target_link_libraries(zef PUBLIC range-v3)
 target_link_libraries(zef PRIVATE doctest)
@@ -157,17 +153,18 @@ endif()
 
 set_target_properties(zef PROPERTIES
   CXX_STANDARD 17
-  COMPILE_DEFINITIONS
-  "DOCTEST_CONFIG_DISABLE;BUILDING_LIBZEF"
   CXX_VISIBILITY_PRESET hidden
   # This is apparently something that needs a check for all compilers, so will disable for now.
   # INTERPROCEDURAL_OPTIMIZATION ON
   # PUBLIC_HEADER "${HEADERS}"
   )
-# if(CMAKE_BUILD_TYPE STREQUAL "Debug")
-#   set_target_properties(zef PROPERTIES
-#     CXX_VISIBILITY_PRESET default)
-# endif()
+target_compile_definitions(zef PRIVATE DOCTEST_CONFIG_DISABLE BUILDING_LIBZEF)
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+  if(NOT WIN32)
+    target_compile_definitions(zef PRIVATE DEBUG)
+    target_compile_options(zef PRIVATE -rdynamic -ggdb -g)
+  endif()
+endif()
 
 if(ZEFDB_ALLOW_NO_TLS)
   target_compile_definitions(zef PUBLIC ZEFDB_ALLOW_NO_TLS)
diff --git a/core/auth.html b/core/auth.html
index c38e6112..5f59faa3 100644
--- a/core/auth.html
+++ b/core/auth.html
@@ -26,6 +26,8 @@
         <div id="firebaseui-auth-container"></div>
         <div id="loader">Loading...</div>
 
+        <p style="text-align: center;">By registering you agree to our <a href="https://console.zefhub.io/terms-of-service" target="_blank">terms of service</a>.</p>
+
         <!-- Signin as guest button -->
         <div class="firebaseui-container firebaseui-page-provider-sign-in firebaseui-id-page-provider-sign-in firebaseui-use-spinner">
             <div class="firebaseui-card-content">
diff --git a/core/external/external.cmake b/core/external/external.cmake
index 976ff7c2..ae63f511 100644
--- a/core/external/external.cmake
+++ b/core/external/external.cmake
@@ -147,13 +147,15 @@ else()
   if(APPLE)
     # Brew on macos doesn't link these things in by default to not shadow the main macos openssl 
     execute_process(
-      COMMAND brew --prefix openssl
+      COMMAND brew --prefix openssl@1.1
       RESULT_VARIABLE BREW_RESULT
       OUTPUT_VARIABLE BREW_OPENSSL_PATH
       OUTPUT_STRIP_TRAILING_WHITESPACE)
     if (BREW_RESULT EQUAL 0 AND EXISTS "${BREW_OPENSSL_PATH}")
+      message(STATUS "Adding '${BREW_OPENSSL_PATH}' to cmake prefix path")
       list(APPEND CMAKE_PREFIX_PATH ${BREW_OPENSSL_PATH})
     endif()
+    message(STATUS "Brew prefix check result was: ${BREW_RESULT}")
   endif()
 
   find_package(OpenSSL QUIET)
@@ -178,8 +180,12 @@ else()
       endif()
     endif()
     if(NOT OPENSSL_FOUND)
-      find_library(OPENSSL_LIBRARIES ssl)
-      find_path(OPENSSL_INCLUDE_DIR openssl/ssl.h)
+      if (APPLE AND DEFINED BREW_OPENSSL_PATH)
+        set(OPENSSL_OTHER_SEARCH_PATH "${BREW_OPENSSL_PATH}")
+        message(STAUS "Going to use extra openssl_other_search_path")
+      endif()
+      find_library(OPENSSL_LIBRARIES ssl PATHS ${OPENSSL_OTHER_SEARCH_PATH})
+      find_path(OPENSSL_INCLUDE_DIR openssl/ssl.h PATHS ${OPENSSL_OTHER_SEARCH_PATH})
       if(OPENSSL_LIBRARIES AND OPENSSL_INCLUDE_DIR)
         set(OPENSSL_FOUND TRUE)
         message(STATUS "Found openssl with find_library and find_path")
@@ -218,6 +224,22 @@ ManualFetchContent_MakeAvailable(nlohmann_json)
 
 create_license_file("nlohmann_json" ${nlohmann_json_SOURCE_DIR}/LICENSE.MIT NO "This library bundles Niels Lohmann's JSON library (https://github.com/nlohmann/json)\n\nFrom distribution point of (https://github.com/ArthurSonzogni/nlohmann_json_cmake_fetchcontent).\n\nThe full text of the nlohmann_json license follows below.\n\n")
 
+# * yaml-cpp
+message(STATUS "External: yaml-cpp")
+
+FetchContent_Declare(yaml-cpp
+  GIT_REPOSITORY https://github.com/jbeder/yaml-cpp
+  GIT_TAG yaml-cpp-0.7.0
+  GIT_SHALLOW ON
+  UPDATE_COMMAND "")
+
+ManualFetchContent_MakeAvailable(yaml-cpp)
+set_target_properties(yaml-cpp PROPERTIES
+  POSITION_INDEPENDENT_CODE ON)
+
+create_license_file("yaml-cpp" ${LAST_SOURCE_DIR}/LICENSE NO "This library bundles the yaml-cpp library (https://github.com/jbeder/yaml-cpp)\n\nThe full text of the yaml-cpp license follows below.\n\n")
+
+
 # * asio
 
 message(STATUS "External: asio")
diff --git a/core/include/blobs.h b/core/include/blobs.h
index de6fe139..951c6d03 100644
--- a/core/include/blobs.h
+++ b/core/include/blobs.h
@@ -15,13 +15,15 @@
  */
 
 #pragma once
+
+#include "export_statement.h"
+
 #include <ostream>
 #include <iomanip>      // std::setprecision
 #include <string_view>
 #include <optional>
 #include <variant>
 #include "constants.h"
-#include "export_statement.h"
 #include "range/v3/all.hpp"
 
 #include "fwd_declarations.h"
diff --git a/core/include/butler/auth.h b/core/include/butler/auth.h
index 97b1e13e..3ded495d 100644
--- a/core/include/butler/auth.h
+++ b/core/include/butler/auth.h
@@ -16,10 +16,10 @@
 
 #pragma once
 
-#include <string>
-
 #include "export_statement.h"
 
+#include <string>
+
 namespace zefDB {
     LIBZEF_DLL_EXPORTED std::string get_firebase_refresh_token_email(std::string key_string);
     LIBZEF_DLL_EXPORTED std::string get_firebase_refresh_token_email(std::string email, std::string password);
diff --git a/core/include/butler/butler.h b/core/include/butler/butler.h
index 688cc320..b05e1c9d 100644
--- a/core/include/butler/butler.h
+++ b/core/include/butler/butler.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "export_statement.h"
+
 #include <thread>
 #include "zefDB_utils.h"
 #include "scalars.h"
@@ -82,8 +84,6 @@ namespace zefDB {
             
 
         std::filesystem::path zefdb_config_path();
-        std::string get_default_zefhub_uri();
-        std::string get_auto_connect();
 
         // If this is set, the butler no longer needs to commmunicate with
         // upstream to ask permission or take primary etc...
@@ -273,6 +273,7 @@ namespace zefDB {
             // wait_for_auth: will start_connection if not already connected
             bool wait_for_auth(std::chrono::duration<double> timeout=std::chrono::seconds(-1));
             void determine_refresh_token();
+            std::string who_am_i();
             // ensure_auth_credentials: if no credentials will pop up browser
             void ensure_auth_credentials();
             std::optional<std::string> load_forced_zefhub_key();
@@ -287,7 +288,6 @@ namespace zefDB {
             std::optional<std::string> session_auth_key;
 
 
-            // Butler() : Butler(get_default_zefhub_uri()) {}
             Butler(std::string uri);
 
 
@@ -416,11 +416,18 @@ namespace zefDB {
         ////////////////////////////////////////
         // * External handlers
 
+        // ** Merge handler
         typedef json (merge_handler_t)(Graph, const json &);
         LIBZEF_DLL_EXPORTED void register_merge_handler(std::function<merge_handler_t> func);
         LIBZEF_DLL_EXPORTED void remove_merge_handler();
         LIBZEF_DLL_EXPORTED json pass_to_merge_handler(Graph g, const json & payload);
 
+        // ** Schema validator
+        typedef void (schema_validator_t)(ZefRef);
+        LIBZEF_DLL_EXPORTED void register_schema_validator(std::function<schema_validator_t> func);
+        LIBZEF_DLL_EXPORTED void remove_schema_validator();
+        LIBZEF_DLL_EXPORTED void pass_to_schema_validator(ZefRef tx);
+
 
 
         ////////////////////////////////////////////////
diff --git a/core/include/butler/butler.hpp b/core/include/butler/butler.hpp
index 981cfa59..851e1fba 100644
--- a/core/include/butler/butler.hpp
+++ b/core/include/butler/butler.hpp
@@ -59,35 +59,33 @@ namespace zefDB {
             auto future = msg_push_internal(std::move(content), ignore_closed);
             std::future_status status;
             assert(timeout.unit == EN.Unit.seconds);
-            if(timeout.value == 0)
-                status = std::future_status::ready;
-            else
-                status = future.wait_for(std::chrono::duration<double>(timeout.value));
-            if(status != std::future_status::ready) {
-                throw std::runtime_error("Butler did not return with response in time after waiting for: " + to_str(timeout.value) + ".");
-            } else {
-                Messages::Response response;
-                try {
-                    response = future.get();
-                } catch(const std::exception & e) {
-                    // std::cerr << "Exception: " << e.what() << std::endl;
-                    throw std::runtime_error("Got unexpected butler error when processing message of type: " + msg_type + "\nException was: " + e.what());
-                }
+            // This needs to be changed to timeout on the butler side instead of here.
+            // if(timeout.value == 0)
+            //     status = std::future_status::ready;
+            // else
+            //     status = future.wait_for(std::chrono::duration<double>(timeout.value));
+            Messages::Response response;
+            try {
+                response = future.get();
+            } catch(const std::exception & e) {
+                // std::cerr << "Exception: " << e.what() << std::endl;
+                throw std::runtime_error("Got unexpected butler error when processing message of type: " + msg_type + "\nException was: " + e.what());
+            }
 
-                return std::visit(overloaded {
-                        [](const T & response) { return response; },
-                        [](const auto & response) -> T {
-                            if constexpr(std::is_constructible_v<T, decltype(response)>) {
-                                return T(response);
-                            } else {
-                                std::string name = typeid(response).name();
-                                std::string desired = typeid(T).name();
-                                std::cerr << "Response from ZefHub is not of the right type. " + name + " not " + desired + "." << std::endl;
-                                throw std::runtime_error("Response from ZefHub is not of the right type. " + name + " not " + desired + ".");
-                            }
+            return std::visit(overloaded {
+                    [](const T & response) { return response; },
+                    [](const auto & response) -> T {
+                        if constexpr(std::is_constructible_v<T, decltype(response)>) {
+                            return T(response);
+                        } else {
+                            std::string name = typeid(response).name();
+                            std::string desired = typeid(T).name();
+                            std::cerr << "Response from ZefHub is not of the right type. " + name + " not " + desired + "." << std::endl;
+                            throw std::runtime_error("Response from ZefHub is not of the right type. " + name + " not " + desired + ".");
                         }
-                    }, response);
-            }
+                    }
+                }, response);
+
         }
     }
 }
diff --git a/core/include/butler/communication.h b/core/include/butler/communication.h
index 3c381216..570bd590 100644
--- a/core/include/butler/communication.h
+++ b/core/include/butler/communication.h
@@ -102,7 +102,38 @@ namespace zefDB {
 
             std::unique_ptr<std::thread> ws_thread;
             client_t endpoint;
-            conn_t con;
+            // Note that con can be modified from the main thread or the ws
+            // thread. Hence it should be locked behind atomic accesses. But
+            // since this is a variant<shared_ptr> nesting, we are not going to
+            // atomically lock the variant mutation. The variant mutation should
+            // happen only once, when the endpoint is created. Thenafter it'll
+            // be only shared_ptr accesses to be locked behind atomic accesses.
+            //
+            // The utility func visit_con is handy for this
+            conn_t _con;
+
+            template<class FUNC>
+            auto visit_con(const FUNC & f) {
+#if ZEFDB_ALLOW_NO_TLS
+                std::visit([this, &f](auto & _con) {
+#endif
+                    auto con = atomic_load(&_con);
+                    return f(con);
+#if ZEFDB_ALLOW_NO_TLS
+                }, _con);
+#endif
+            }
+
+            template<class FUNC>
+            auto visit_endpoint(const FUNC & f) {
+#if ZEFDB_ALLOW_NO_TLS
+                std::visit([this, &f](auto & endpoint) {
+#endif
+                    return f(endpoint);
+#if ZEFDB_ALLOW_NO_TLS
+                }, endpoint);
+#endif
+            }
             
             ////////
             // Managing vars
@@ -111,7 +142,7 @@ namespace zefDB {
             std::atomic_bool wspp_in_control = false;
             bool last_was_failure = false;
             std::chrono::time_point<std::chrono::steady_clock> last_connect_time;
-            bool should_stop = false;
+            std::atomic_bool should_stop = false;
             std::chrono::duration<double> ping_interval = std::chrono::seconds(15);
             int allowed_silent_failures = 0;
 
diff --git a/core/include/butler/locking.h b/core/include/butler/locking.h
index 08cf867b..da2e15d9 100644
--- a/core/include/butler/locking.h
+++ b/core/include/butler/locking.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "export_statement.h"
+
 #include <atomic>
 #include <chrono>
 #include <mutex>
diff --git a/core/include/butler/messages.h b/core/include/butler/messages.h
index 24105760..f345a770 100644
--- a/core/include/butler/messages.h
+++ b/core/include/butler/messages.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "export_statement.h"
+
 #include "fwd_declarations.h"
 #include "constants.h"
 #include "graph.h"
diff --git a/core/include/butler/msgqueue.h b/core/include/butler/msgqueue.h
index 0c19af92..6bf659d9 100644
--- a/core/include/butler/msgqueue.h
+++ b/core/include/butler/msgqueue.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "export_statement.h"
+
 #include "fwd_declarations.h"
 #include "graph.h"
 #include "butler/locking.h"
diff --git a/core/include/butler/threadsafe_map.h b/core/include/butler/threadsafe_map.h
index 8d51da33..b735ca18 100644
--- a/core/include/butler/threadsafe_map.h
+++ b/core/include/butler/threadsafe_map.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "export_statement.h"
+
 #include <unordered_map>
 #include <shared_mutex>
 #include <mutex>
diff --git a/core/include/butler/utils.h b/core/include/butler/utils.h
index 841c803d..bf9a46d3 100644
--- a/core/include/butler/utils.h
+++ b/core/include/butler/utils.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "export_statement.h"
+
 // STOLEN FROM CPPREFERENCE
 // helper type for the visitor #4
 template<class... Ts> struct overloaded : Ts... { using Ts::operator()...; };
diff --git a/core/include/export_statement.h.in b/core/include/export_statement.h.in
index fcc1e3f8..1f4b3e69 100644
--- a/core/include/export_statement.h.in
+++ b/core/include/export_statement.h.in
@@ -16,6 +16,11 @@
 
 #pragma once
 
+#cmakedefine LIBZEF_FORCE_ASSERTS
+#ifdef LIBZEF_FORCE_ASSERTS
+#undef NDEBUG
+#include <cassert>
+#endif
 
 #cmakedefine LIBZEF_STATIC
 #ifdef LIBZEF_STATIC
@@ -36,7 +41,11 @@
 #endif
 #endif
 
-#ifdef _MSC_VER
+#if defined(_MSC_VER) || defined(WIN32) || defined(__WIN32__)
+#define ZEF_WIN32 1
+#endif
+
+#ifdef ZEF_WIN32
 
 // Needed to avoid issues with winsock.h and winsock2.h
 #define WIN32_LEAN_AND_MEAN
diff --git a/core/include/fwd_declarations.h b/core/include/fwd_declarations.h
index 087b96c9..78e0b7e7 100644
--- a/core/include/fwd_declarations.h
+++ b/core/include/fwd_declarations.h
@@ -33,7 +33,7 @@
 
 
 // Will just not deal with stack traces on windows
-#ifdef _MSC_VER
+#ifdef ZEF_WIN32
 inline void print_backtrace_force(int fd=2) {}
 inline void print_backtrace(int fd=2) {}
 #else
diff --git a/core/include/graph.h b/core/include/graph.h
index 21a9cfd0..e0a470c5 100644
--- a/core/include/graph.h
+++ b/core/include/graph.h
@@ -15,6 +15,9 @@
  */
 
 #pragma once
+
+#include "export_statement.h"
+
 #include <iostream>
 // #include <unordered_map>
 #include <variant>
@@ -472,8 +475,12 @@ namespace zefDB {
     LIBZEF_DLL_EXPORTED ZefRef StartTransactionReturnTx(GraphData& gd) ;
     inline ZefRef StartTransactionReturnTx(Graph & g) { return StartTransactionReturnTx(g.my_graph_data()); }
 
-    LIBZEF_DLL_EXPORTED void FinishTransaction(GraphData& gd, bool wait) ;
+    LIBZEF_DLL_EXPORTED void FinishTransaction(GraphData& gd, bool wait, bool rollback_empty_tx, bool check_schema);
+    LIBZEF_DLL_EXPORTED void FinishTransaction(GraphData& gd, bool wait, bool rollback_empty_tx);
+    LIBZEF_DLL_EXPORTED void FinishTransaction(GraphData& gd, bool wait);
     LIBZEF_DLL_EXPORTED void FinishTransaction(GraphData& gd) ;
+    inline void FinishTransaction(Graph & g, bool wait, bool rollback_empty_tx, bool check_schema) { FinishTransaction(g.my_graph_data(), wait, rollback_empty_tx, check_schema); }
+    inline void FinishTransaction(Graph & g, bool wait, bool rollback_empty_tx) { FinishTransaction(g.my_graph_data(), wait, rollback_empty_tx); }
     inline void FinishTransaction(Graph & g, bool wait) { FinishTransaction(g.my_graph_data(), wait); }
     inline void FinishTransaction(Graph & g) { FinishTransaction(g.my_graph_data()); }
 
@@ -488,21 +495,31 @@ namespace zefDB {
         // Danny: I can't see where this is used so I'm commenting it out for now.
 		// bool this_transaction_already_unregistered_from_graph = false;
         bool wait;
+        bool rollback_empty;
+        bool check_schema;
 			
 		Transaction() = delete;
 		Transaction(const Transaction&) = delete;
 		Transaction(Transaction&&) = delete;  // this could be weakened and implemented if needed
+        // The default behaviour is based on zwitch so we have to do things the long-winded way here.
 		Transaction(GraphData& gd);
-		Transaction(GraphData& gd, bool wait) : wait(wait) {
+		Transaction(GraphData& gd, bool wait);
+		Transaction(GraphData& gd, bool wait, bool rollback_empty);
+		Transaction(GraphData& gd, bool wait, bool rollback_empty, bool check_schema) :
+        wait(wait),
+        rollback_empty(rollback_empty),
+        check_schema(check_schema) {
             StartTransaction(gd);
             graph_data_that_i_am_marking.blob_ptr = &gd;
 		}
 		Transaction(Graph& g) : Transaction(g.my_graph_data()) {}
-		Transaction(Graph& g, bool wait): Transaction(g.my_graph_data(), wait) {}
+		Transaction(Graph& g, bool wait) : Transaction(g.my_graph_data(), wait) {}
+		Transaction(Graph& g, bool wait, bool rollback_empty) : Transaction(g.my_graph_data(), wait, rollback_empty) {}
+		Transaction(Graph& g, bool wait, bool rollback_empty, bool check_schema) : Transaction(g.my_graph_data(), wait, rollback_empty, check_schema) {}
 		~Transaction() {
             GraphData& gd = *(GraphData*)graph_data_that_i_am_marking.blob_ptr;
 			try {
-				FinishTransaction(gd, wait);
+				FinishTransaction(gd, wait, rollback_empty, check_schema);
 			}
 			catch (const std::exception& exc) {
 				std::cerr << "Error occurred on Transaction closing: " << exc.what() << "\n";
diff --git a/core/include/high_level_api.h b/core/include/high_level_api.h
index d4badf30..41ac32f0 100644
--- a/core/include/high_level_api.h
+++ b/core/include/high_level_api.h
@@ -15,6 +15,9 @@
  */
 
 #pragma once
+
+#include "export_statement.h"
+
 #include <type_traits>  // std::is_same<T, animal>::value
 #include <unordered_map>
 #include <algorithm>
@@ -1907,7 +1910,7 @@ namespace zefDB {
                                  
 		inline EZefRefs InstancesEternal::operator() (const Graph& g) const {			
             // If the delegate doesn't exist, an exception will be thrown. Use this to return an empty list
-                return std::visit(overload{
+                return std::visit(overloaded{
                         [&g](Instances::Sentinel sent) { return internals::all_raes(g); },
                         [&g](auto tp) {
                             std::optional<EZefRef> temp = g | delegate[tp];
@@ -2203,7 +2206,10 @@ namespace zefDB {
 			case BlobType::ENTITY_NODE: return true;
 			case BlobType::ATOMIC_ENTITY_NODE: return true;
 			case BlobType::RELATION_EDGE: return true;
-			default: throw std::runtime_error("attempting to link a blob that cannot be linked via a relation");
+			default: {
+                // print_backtrace();
+                throw std::runtime_error("asserting is a RAE failed");
+            }
 			}                    
 		};                       
                                  
@@ -2216,7 +2222,10 @@ namespace zefDB {
 			case BlobType::RELATION_EDGE: return true;
 			case BlobType::TX_EVENT_NODE: return true;
 			case BlobType::ROOT_NODE: return true;
-			default: throw std::runtime_error("attempting to link a blob that cannot be linked via a relation");
+			default: {
+                // print_backtrace();
+                throw std::runtime_error("attempting to link a blob that cannot be linked via a relation");
+            }
 			}                    
 		};                       
                                  
@@ -2238,6 +2247,17 @@ namespace zefDB {
 			return my_entity_or_rel; // hack to suppress compiler warnings
 		}                        
 
+		inline EZefRef get_TO_DELEGATE_EDGE(EZefRef my_entity_or_rel) {
+			for (auto ind : AllEdgeIndexes(my_entity_or_rel)) {
+				if (ind < 0) {   
+					auto candidate = EZefRef(abs_val(ind), *graph_data(my_entity_or_rel));
+					if (get<BlobType>(candidate) == BlobType::TO_DELEGATE_EDGE) return candidate;
+				}                
+			}                    
+			throw std::runtime_error("We should not have landed here in get_TO_DELEGATE_EDGE: there should have been one el to return");
+			return my_entity_or_rel; // hack to suppress compiler warnings
+		}                        
+
 		LIBZEF_DLL_EXPORTED bool is_terminated(EZefRef my_rel_ent);
 
 
@@ -2339,6 +2359,8 @@ namespace zefDB {
 		LIBZEF_DLL_EXPORTED bool exists_at(ZefRef, EZefRef tx);
 		LIBZEF_DLL_EXPORTED bool exists_at(ZefRef, ZefRef tx);
 
+		LIBZEF_DLL_EXPORTED bool exists_at_now(EZefRef);
+
         LIBZEF_DLL_EXPORTED ZefRef to_frame(EZefRef uzr, EZefRef tx, bool allow_terminated=false);
         LIBZEF_DLL_EXPORTED ZefRef to_frame(ZefRef uzr, EZefRef tx, bool allow_terminated=false);
         LIBZEF_DLL_EXPORTED ZefRef to_frame(EZefRef uzr, ZefRef tx, bool allow_terminated=false);
diff --git a/core/include/low_level_api.h b/core/include/low_level_api.h
index 430441a5..a52d2dfc 100644
--- a/core/include/low_level_api.h
+++ b/core/include/low_level_api.h
@@ -15,6 +15,9 @@
  */
 
 #pragma once
+
+#include "export_statement.h"
+
 #include <random>
 #include <chrono>
 #include <ctime>
diff --git a/core/include/mmap.h b/core/include/mmap.h
index 13b458ad..3a36bab3 100644
--- a/core/include/mmap.h
+++ b/core/include/mmap.h
@@ -16,11 +16,12 @@
 
 #pragma once
 
-#ifdef _MSC_VER
+#include "export_statement.h"
+
+#ifdef ZEF_WIN32
 #include "mmap_windows.h"
 #else
 
-
 #include <iostream>
 #include <vector>
 #include <stdexcept>
@@ -42,7 +43,6 @@
 #include <shared_mutex>
 #include <atomic>
 
-#include "export_statement.h"
 #include "constants.h"
 #include "zefDB_utils.h"
 #include "uids.h"
@@ -388,16 +388,18 @@ namespace zefDB {
 #else
         inline void ensure_or_alloc_range(void * ptr, size_t size) {
 #endif
-#ifndef _MSC_VER
-            // This is a very dirty hack to ensure we never alloc something too small
-            if(size < blobs_ns::max_basic_blob_size) {
-                std::cerr << "Warning! Tried to ensure a mem range that is smaller than max_basic_blob_size!" << std::endl;
-                // Can't use print_backtrace here due to cyclic deps.
-                void *array[50];
-                size_t size;
-                size = backtrace(array, 50);
-                backtrace_symbols_fd(array, size, 1);
-            }
+#ifndef ZEF_WIN32
+            // We don't do the following check anymore, as there are cases where a single blob_index-size ensure is needed.
+
+            // // This is a very dirty hack to ensure we never alloc something too small
+            // if(size < blobs_ns::max_basic_blob_size) {
+            //     std::cerr << "Warning! Tried to ensure a mem range that is smaller than max_basic_blob_size!" << std::endl;
+            //     // Can't use print_backtrace here due to cyclic deps.
+            //     void *array[50];
+            //     size_t size;
+            //     size = backtrace(array, 50);
+            //     backtrace_symbols_fd(array, size, 1);
+            // }
 #endif
             
             MMapAllocInfo& info = info_from_blob(ptr);
diff --git a/core/include/mmap_windows.h b/core/include/mmap_windows.h
index 3e454527..811e8073 100644
--- a/core/include/mmap_windows.h
+++ b/core/include/mmap_windows.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "export_statement.h"
+
 #include <iostream>
 #include <vector>
 #include <stdexcept>
@@ -37,7 +39,6 @@
 #include <shared_mutex>
 #include <atomic>
 
-#include "export_statement.h"
 #include "constants.h"
 #include "zefDB_utils.h"
 #include "uids.h"
diff --git a/core/include/observable.h b/core/include/observable.h
index 4795169a..0065d759 100644
--- a/core/include/observable.h
+++ b/core/include/observable.h
@@ -15,6 +15,9 @@
  */
 
 #pragma once
+
+#include "export_statement.h"
+
 #include <vector>
 #include <unordered_map>
 #include <functional>
diff --git a/core/include/scalars.h b/core/include/scalars.h
index 9f5ab102..ffc321cc 100644
--- a/core/include/scalars.h
+++ b/core/include/scalars.h
@@ -15,6 +15,9 @@
  */
 
 #pragma once
+
+#include "export_statement.h"
+
 //#include "graph.h"
 #include <ostream>
 #include <iostream>
diff --git a/core/include/synchronization.h b/core/include/synchronization.h
index 048604f1..85f02c30 100644
--- a/core/include/synchronization.h
+++ b/core/include/synchronization.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "export_statement.h"
+
 #include "fwd_declarations.h"
 #include "scalars.h"
 #include "zefref.h"
@@ -108,6 +110,7 @@ namespace zefDB {
 
 
         LIBZEF_DLL_EXPORTED void apply_double_linking(GraphData& gd, blob_index start_index, blob_index end_index);
+        LIBZEF_DLL_EXPORTED void undo_double_linking(GraphData& gd, blob_index start_index, blob_index end_index);
         LIBZEF_DLL_EXPORTED void apply_actions_to_blob_range_only_key_dict(GraphData& gd, blob_index blob_index_lo, blob_index blob_index_hi);
 
 
diff --git a/core/include/tensor.h b/core/include/tensor.h
index 5bfc8f0c..d22524ca 100644
--- a/core/include/tensor.h
+++ b/core/include/tensor.h
@@ -15,6 +15,9 @@
  */
 
 #pragma once
+
+#include "export_statement.h"
+
 #include "blobs.h"
 #include <array>
 #include <variant>
@@ -23,12 +26,6 @@
 #include <initializer_list>
 
 namespace zefDB {
-	// ------- use variadic templates: see  https://www.bfilipek.com/2018/09/visit-variants.html--------------------------
-	// Is C++ really making it that difficult to do a bit of pattern matching below? 
-	template<class... Ts> struct overload : Ts... { using Ts::operator()...; };
-	template<class... Ts> overload(Ts...)->overload<Ts...>;
-
-
 
 	template <typename ScalarType, int TensorOrder>
 	struct Tensor {};
diff --git a/core/include/tokens.h b/core/include/tokens.h
index ad45888b..b374de59 100644
--- a/core/include/tokens.h
+++ b/core/include/tokens.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "export_statement.h"
+
 #include "fwd_declarations.h"
 #include "append_structures.h"
 #include "butler/threadsafe_map.h"
@@ -438,27 +440,6 @@ namespace zefDB {
 
     // Note: a delegate of "order 0" is a representation of an instance.
 
-    struct LIBZEF_DLL_EXPORTED DelegateEntity {
-        EntityType et;
-        bool operator==(const DelegateEntity & other) const {
-            return et == other.et;
-        }
-    };
-    LIBZEF_DLL_EXPORTED std::ostream & operator<<(std::ostream & o, const DelegateEntity & d);
-    struct LIBZEF_DLL_EXPORTED DelegateAtomicEntity {
-        AtomicEntityType aet;
-        bool operator==(const DelegateAtomicEntity & other) const {
-            return aet == other.aet;
-        }
-    };
-    LIBZEF_DLL_EXPORTED std::ostream & operator<<(std::ostream & o, const DelegateAtomicEntity & d);
-    struct LIBZEF_DLL_EXPORTED DelegateRelationGroup {
-        RelationType rt;
-        bool operator==(const DelegateRelationGroup & other) const {
-            return rt == other.rt;
-        }
-    };
-    LIBZEF_DLL_EXPORTED std::ostream & operator<<(std::ostream & o, const DelegateRelationGroup & d);
     struct LIBZEF_DLL_EXPORTED DelegateTX {
         bool operator==(const DelegateTX & other) const { return true; }
     };
@@ -485,19 +466,22 @@ namespace zefDB {
     struct LIBZEF_DLL_EXPORTED Delegate {
         int order;
         using var_t = std::variant<
-            DelegateEntity,
-            DelegateAtomicEntity,
-            DelegateRelationGroup,
+            EntityType,
+            AtomicEntityType,
+            RelationType,
+
             DelegateRelationTriple,
             DelegateTX,
             DelegateRoot
             >;
         var_t item;
         Delegate(int order, var_t item) : order(order), item(item) {}
-        Delegate(EntityType et) : order(0), item(DelegateEntity{et}) {}
-        Delegate(AtomicEntityType aet) : order(0), item(DelegateAtomicEntity{aet}) {}
-        Delegate(RelationType rt) : order(0), item(DelegateRelationGroup{rt}) {}
+        Delegate(EntityType et) : order(0), item(et) {}
+        Delegate(AtomicEntityType aet) : order(0), item(aet) {}
+        Delegate(RelationType rt) : order(0), item(rt) {}
         Delegate(Delegate source, RelationType rt, Delegate target) : order(0), item(DelegateRelationTriple{rt,source,target}) {}
+        Delegate(const Delegate& other) = default;
+        Delegate(Delegate&& other) = default;
 
         bool operator==(const Delegate & other) const {
             bool same_item = std::visit([&](auto & left, auto & right) -> bool {
diff --git a/core/include/tools.h b/core/include/tools.h
index d3aa1107..f285b9d1 100644
--- a/core/include/tools.h
+++ b/core/include/tools.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "export_statement.h"
+
 #include <fstream>
 #include <functional>
 #include <variant>
diff --git a/core/include/uids.h b/core/include/uids.h
index e9364ba8..811e04d7 100644
--- a/core/include/uids.h
+++ b/core/include/uids.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "export_statement.h"
+
 #include "zefDB_utils.h"
 #include <nlohmann/json.hpp>
 using json = nlohmann::json;
diff --git a/core/include/verification.h b/core/include/verification.h
index 86abca76..a765b329 100644
--- a/core/include/verification.h
+++ b/core/include/verification.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "export_statement.h"
+
 #include "fwd_declarations.h"
 #include "zefDB_utils.h"
 #include "scalars.h"
@@ -27,26 +29,24 @@
 #include "low_level_api.h"
 
 namespace zefDB {
-	namespace verification {	
-
+    namespace verification {	
 
 
-		// make go to the both the src and trg blobs: there check that indx of start blob is in the edge_list exactly once. Else throw.
-      LIBZEF_DLL_EXPORTED bool verify_that_my_index_is_in_source_target_node_edge_list(EZefRef uzr);
-		
 
-      LIBZEF_DLL_EXPORTED bool verify_that_all_uzrs_in_my_edgelist_refer_to_me(EZefRef uzr);
+        // make go to the both the src and trg blobs: there check that indx of start blob is in the edge_list exactly once. Else throw.
+        LIBZEF_DLL_EXPORTED bool verify_that_my_index_is_in_source_target_node_edge_list(EZefRef uzr);
+        
 
+        LIBZEF_DLL_EXPORTED bool verify_that_all_uzrs_in_my_edgelist_refer_to_me(EZefRef uzr);
 
-		// check low level graph that double linking of node / edges with indexes is consistent
-      LIBZEF_DLL_EXPORTED bool verify_graph_double_linking(Graph& g);
+        // check low level graph that double linking of node / edges with indexes is consistent
+        LIBZEF_DLL_EXPORTED bool verify_graph_double_linking(Graph& g);
+        LIBZEF_DLL_EXPORTED bool verify_chronological_instantiation_order(Graph g);
         LIBZEF_DLL_EXPORTED void break_graph(Graph&g, blob_index index, int style);
 
+        inline bool verify_graph(Graph&g) {
+            return (verify_graph_double_linking(g)
+                    && verify_chronological_instantiation_order(g));
+        }
 	}
-
-
-
-
-
-
 }
diff --git a/core/include/zefDB.h b/core/include/zefDB.h
index f6b3531f..903bc0d1 100644
--- a/core/include/zefDB.h
+++ b/core/include/zefDB.h
@@ -18,6 +18,7 @@
 
 #pragma once
 
+#include "export_statement.h"
 
 #include "fwd_declarations.h"
 #include "zefDB_utils.h"
@@ -41,3 +42,4 @@
 #include "butler/butler_zefhub.h"
 
 #include "from_json.h"
+#include "zef_config.h"
\ No newline at end of file
diff --git a/core/include/zefDB_utils.h b/core/include/zefDB_utils.h
index 63500337..a51d9b63 100644
--- a/core/include/zefDB_utils.h
+++ b/core/include/zefDB_utils.h
@@ -15,6 +15,9 @@
  */
 
 #pragma once
+
+#include "export_statement.h"
+
 #include <vector>
 #include <array>
 #include <unordered_map>
@@ -32,7 +35,6 @@
 #include "include_fs.h"
 
 // #include "fwd_declarations.h"
-#include "export_statement.h"
 #include "constants.h"
 
 #include "range/v3/all.hpp"
@@ -263,6 +265,21 @@ namespace zefDB {
         ~RAII_CallAtEnd() { func(); }
     };
 
+    inline bool parse_string_bool(std::string s) {
+        if(s == "0" ||
+           s == "NO" ||
+           s == "no" ||
+           s == "FALSE" ||
+           s == "false")
+            return false;
+        if(s == "1" ||
+           s == "YES" ||
+           s == "yes" ||
+           s == "TRUE" ||
+           s == "true")
+            return true;
+        throw std::runtime_error("Unknown string to convert to bool: '" + s + "'");
+    }
     inline bool check_env_bool(const char * var, bool default_val=false) {
         char * env = std::getenv(var);
         if(env == nullptr)
@@ -272,20 +289,11 @@ namespace zefDB {
         if(s_env == "")
             return default_val;
 
-        if(s_env == "0" ||
-           s_env == "NO" ||
-           s_env == "no" ||
-           s_env == "FALSE" ||
-           s_env == "false")
-            return false;
-        else if(s_env == "1" ||
-                s_env == "YES" ||
-                s_env == "yes" ||
-                s_env == "TRUE" ||
-                s_env == "true")
-            return true;
-        else
+        try {
+            return parse_string_bool(s_env);
+        } catch(...) {
             std::cerr << "Warning, found value for environment variable " << var << "='" << s_env << "' but was not recognised. Value should be one of 0, 1, NO, YES, FALSE, TRUE." << std::endl;
+        }
 
         return default_val;
     }
@@ -307,18 +315,22 @@ namespace zefDB {
     }
 
     // Stealing from StackOverflow: https://stackoverflow.com/questions/20112221/invoking-a-function-automatically-on-stdthread-exit-in-c11
-    inline void on_thread_exit(std::function<void()> func) {
+    inline void on_thread_exit(std::string name, std::function<void()> func) {
         thread_local struct ThreadExiter {
-            std::deque<std::function<void()>> callbacks;
+            std::deque<std::pair<std::string, std::function<void()>>> callbacks;
 
             ~ThreadExiter() {
-                for (auto &callback: callbacks) {
+                for (auto &p: callbacks) {
+                    auto & name = std::get<0>(p);
+                    auto & callback = std::get<1>(p);
+                    // std::cerr << "Going to call one callback: " << name << std::endl;
                     callback();
+                    // std::cerr << "Finished calling callback: " << name << std::endl;
                 }
             }
         } exiter;
 
-        exiter.callbacks.emplace_front(std::move(func));
+        exiter.callbacks.emplace_front(name, std::move(func));
     }
 
 }
diff --git a/core/include/zef_config.h b/core/include/zef_config.h
new file mode 100644
index 00000000..a3d0b23c
--- /dev/null
+++ b/core/include/zef_config.h
@@ -0,0 +1,70 @@
+/*
+ * Copyright 2022 Synchronous Technologies Pte Ltd
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#pragma once
+
+#include "export_statement.h"
+
+#include <iostream>
+#include <variant>
+#include <vector>
+#include <array>
+#include <algorithm>
+
+namespace zefDB {
+
+    // Note: the order needs bool then int then double as pybind will otherwise
+    // cast inputs to a different type.
+    using config_var_t = std::variant<std::string, bool, int, double>;
+
+    struct ConfigItem {
+        std::string path;
+
+        // The default also sets the allowed type of the variable
+        config_var_t default_value;
+
+        // If filled, these are the only allowed options.
+        std::vector<config_var_t> options;
+
+        // Environment variable to override config variable
+        std::string env_var;
+        std::string doc;
+
+        // Need this really specific constructor to avoid issues with const char * converting to bool instead of std::string
+        ConfigItem(const char * path, const char * default_value, std::vector<const char *> options, const char * env_var, const char * doc) :
+            path(path),
+            default_value(std::string(default_value)),
+            env_var(env_var),
+            doc(doc) {
+            std::transform(options.begin(), options.end(), std::back_inserter(this->options), [](const char * opt) { return std::string(opt); });
+        }
+
+        ConfigItem(std::string path, config_var_t default_value, std::vector<config_var_t> options, std::string env_var, std::string doc) :
+            path(path),
+            default_value(default_value),
+            options(options),
+            env_var(env_var),
+            doc(doc) {}
+    };
+
+       
+    LIBZEF_DLL_EXPORTED bool validate_config_file();
+
+    LIBZEF_DLL_EXPORTED config_var_t get_config_var(std::string key);
+    LIBZEF_DLL_EXPORTED void set_config_var(std::string key, config_var_t val);
+    LIBZEF_DLL_EXPORTED std::vector<std::pair<std::string,config_var_t>> list_config(std::string filter="");
+}
+
diff --git a/core/include/zefref.h b/core/include/zefref.h
index 1f9cdd82..a3b525fd 100644
--- a/core/include/zefref.h
+++ b/core/include/zefref.h
@@ -15,6 +15,9 @@
  */
 
 #pragma once
+
+#include "export_statement.h"
+
 #include <iostream>
 #include <array>
 #include <vector>
diff --git a/core/include/zwitch.h b/core/include/zwitch.h
index 28ac5156..c9eba508 100644
--- a/core/include/zwitch.h
+++ b/core/include/zwitch.h
@@ -16,6 +16,8 @@
 
 #pragma once
 
+#include "export_statement.h"
+
 #include "fwd_declarations.h"
 #include "zefDB_utils.h"
 #include <chrono>
@@ -56,7 +58,7 @@ namespace zefDB {
 
         DEFINE_FLAG(throw_on_zefrefs_no_tx, false);
         DEFINE_FLAG(default_wait_for_tx_finish, true);
-        DEFINE_FLAG(write_thread_runs_subscriptions, false);
+        DEFINE_FLAG(default_rollback_empty_tx, true);
 
         Zwitch allow_dynamic_type_definitions(bool new_value) {
             allow_dynamic_entity_type_definitions(new_value);
@@ -81,7 +83,7 @@ namespace zefDB {
             res["debug_times"] = str_to_bool(flag_debug_times);
             res["throw_on_zefrefs_no_tx"] = str_to_bool(flag_throw_on_zefrefs_no_tx);
             res["default_wait_for_tx_finish"] = str_to_bool(flag_default_wait_for_tx_finish);
-            res["write_thread_runs_subscriptions"] = str_to_bool(flag_write_thread_runs_subscriptions);
+            res["default_rollback_empty_tx"] = str_to_bool(flag_default_rollback_empty_tx);
             return res;
         }
 
diff --git a/core/src/butler/auth.cpp b/core/src/butler/auth.cpp
index 55122c31..d5cc856d 100644
--- a/core/src/butler/auth.cpp
+++ b/core/src/butler/auth.cpp
@@ -29,25 +29,40 @@ using json = nlohmann::json;
 #include "butler/auth.h"
 
 namespace zefDB {
+
+    // Curl has all kinds of warnings about thread safety. We try and avoid
+    // these issues by setting curl up as early as possible (on load of library)
+    //
+    // Note that curl easy handles need to be thread local so they don't go into
+    // CurlGlobal.
+    struct CurlGlobal {
+        CurlGlobal() {
+            curl_global_init(CURL_GLOBAL_ALL);
+        }
+
+        ~CurlGlobal() {
+            curl_global_cleanup();
+        }
+    };
+
+    static CurlGlobal curl_global;
+
     CURL* initialise_curl() {
-        thread_local bool done = false;
         thread_local CURL* easy_handle;
+        thread_local bool done = false;
         if(!done) {
-            curl_global_init(CURL_GLOBAL_ALL);
-
             easy_handle = curl_easy_init();
-
-            on_thread_exit([]() {
+            // Suppressing warnings?
+            auto temp = easy_handle;
+            on_thread_exit("curl_cleanup", [temp]() {
                 curl_easy_cleanup(easy_handle);
-                curl_global_cleanup();
             });
-
             done = true;
         }
-
         curl_easy_reset(easy_handle);
         return easy_handle;
     }
+
     size_t _curl_write_data(void * curl_buffer, size_t size, size_t nmemb, void * userp) {
         std::string & buf = *(std::string*)userp;
         assert(size == 1);
diff --git a/core/src/butler/auth_server.cpp b/core/src/butler/auth_server.cpp
index acd71524..91ee6e26 100644
--- a/core/src/butler/auth_server.cpp
+++ b/core/src/butler/auth_server.cpp
@@ -27,7 +27,7 @@ using json = nlohmann::json;
 #include "zwitch.h"
 
 
-#if defined(_MSC_VER)
+#if defined(ZEF_WIN32)
     std::filesystem::path find_libzef_path() {
         char* path = std::getenv("LIBZEF_AUTH_HTML_PATH");
         if (zefDB::zwitch.developer_output())
diff --git a/core/src/butler/butler.cpp b/core/src/butler/butler.cpp
index e2716d4f..7ff77d8a 100644
--- a/core/src/butler/butler.cpp
+++ b/core/src/butler/butler.cpp
@@ -18,9 +18,17 @@
 #include <algorithm>
 #include <chrono>
 #include <fstream>
+
+#include "nlohmann/json.hpp"
+using json = nlohmann::json;
+
+#include <jwt-cpp/jwt.h>
+#include "jwt-cpp/traits/nlohmann-json/traits.h"
+
 // I want to not have to use this:
 #include "high_level_api.h"
 #include "synchronization.h"
+#include "zef_config.h"
 
 namespace zefDB {
     bool initialised_python_core = false;
@@ -32,7 +40,7 @@ namespace zefDB {
 #include "butler_handlers_ws.cpp"
 
         //////////////////////////////////////////////////////////
-        // * Butler lifecycle managment
+        // * Butler lifecycle management
 
         std::shared_ptr<Butler> butler;
         bool butler_is_master = false;
@@ -72,7 +80,7 @@ namespace zefDB {
         }
 
         void terminate_handler() {
-#ifndef _MSC_VER
+#ifndef ZEF_WIN32
             void *trace_elems[20];
             int trace_elem_count(backtrace( trace_elems, 20 ));
             char **stack_syms(backtrace_symbols( trace_elems, trace_elem_count ));
@@ -96,12 +104,15 @@ namespace zefDB {
         }   
 
         void initialise_butler() {
-            initialise_butler(get_default_zefhub_uri());
+            initialise_butler(std::get<std::string>(get_config_var("login.zefhubURL")));
         }
         void initialise_butler(std::string zefhub_uri) {
 #ifdef DEBUG
             internals::static_checks();
 #endif
+            if(!validate_config_file()) {
+                std::cerr << "WARNING: config options are invalid..." << std::endl;
+            }
             
             if(zwitch.zefhub_communication_output())
                 std::cerr << "Will use uri=" << zefhub_uri << " when later connecting to ZefHub" << std::endl;
@@ -131,14 +142,14 @@ namespace zefDB {
 #endif
 
             if(!butler_registered_thread_exiter) {
-                on_thread_exit(&stop_butler);
+                on_thread_exit("stop_butler", &stop_butler);
                 butler_registered_thread_exiter = true;
             }
 
             if(butler->want_upstream_connection()) {
                 // Lock this behind the if check, as we can get into trouble
                 // with python circular deps.
-                auto auto_connect = get_auto_connect();
+                std::string auto_connect = std::get<std::string>(get_config_var("login.autoConnect"));
                 if(auto_connect == "always" ||
                    (auto_connect == "auto" && butler->have_auth_credentials()))
                     butler->start_connection();
@@ -162,7 +173,10 @@ namespace zefDB {
                         std::cerr << "Thread taking a long time to shutdown... " << name << std::endl;
                         if(extra_print)
                             (*extra_print)();
-                        status = future.wait_for(std::chrono::seconds(10));
+                        if(check_env_bool("ZEFDB_DEVELOPER_THREAD_LONGWAIT"))
+                            status = future.wait_for(std::chrono::seconds(10000));
+                        else
+                            status = future.wait_for(std::chrono::seconds(10));
                         if (status == std::future_status::timeout) {
                             std::cerr << "Gave up on waiting for thread: " << name << std::endl;
                             thread.detach();
@@ -179,12 +193,21 @@ namespace zefDB {
             }
         }
         void stop_butler() {
+            std::cerr << "Start of stop_butler" << std::endl;
+            // This is to prevent multiple threads attacking this function at
+            // the same time.
+            static std::atomic<bool> _running = false;
+            bool was_running = _running.exchange(true);
+            if(was_running)
+                return;
+
             if(zwitch.developer_output()) {
                 std::cerr << "stop_butler was called" << std::endl;
             }
 
             if (!butler) {
                 std::cerr << "Butler wasn't running." << std::endl;
+                _running = false;
                 return;
             }
 
@@ -193,20 +216,26 @@ namespace zefDB {
 
             if(zwitch.developer_output())
                 std::cerr << "Going to close all graph manager queues" << std::endl;
-            for(auto data : butler->graph_manager_list) {
-                try {
-                    data->queue.set_closed(false);
-                } catch(const std::exception & e) {
-                    std::cerr << "Exception while trying to close queue for graph manager thread: " << e.what() << std::endl;
+            {
+                std::lock_guard lock(butler->graph_manager_list_mutex);
+                for(auto data : butler->graph_manager_list) {
+                    try {
+                        data->queue.set_closed(false);
+                    } catch(const std::exception & e) {
+                        std::cerr << "Exception while trying to close queue for graph manager thread: " << e.what() << std::endl;
+                    }
                 }
             }
-            // std::cerr << "Going to wait on graph manager threads" << std::endl;
+            if(zwitch.developer_output())
+                std::cerr << "Going to wait on graph manager threads" << std::endl;
             std::vector<std::shared_ptr<Butler::GraphTrackingData>> saved_list;
             {
                 std::lock_guard lock(butler->graph_manager_list_mutex);
                 saved_list = butler->graph_manager_list;
             }
 
+            if(zwitch.developer_output())
+                std::cerr << "Number of graphs to wait for: " << saved_list.size() << std::endl;
             for(auto data : saved_list) {
                 auto extra_print = [&data]() {
                     std::cerr << "Last action was: " << data->debug_last_action << std::endl;
@@ -231,7 +260,11 @@ namespace zefDB {
                 };
                 long_wait_or_kill(*data->managing_thread, data->return_value, data->uid, extra_print);
             }
-            butler->graph_manager_list.clear();
+            // Lock and keep locked for the remainder now
+            {
+                std::lock_guard lock(butler->graph_manager_list_mutex);
+                butler->graph_manager_list.clear();
+            }
             if(zwitch.developer_output())
                 std::cerr << "Removing local process graph" << std::endl;
             butler->local_process_graph.reset();
@@ -239,6 +272,8 @@ namespace zefDB {
             if(zwitch.developer_output())
                 std::cerr << "Stopping network" << std::endl;
             butler->network.stop_running();
+            if(zwitch.developer_output())
+                std::cerr << "Clear waiting tasks" << std::endl;
             butler->waiting_tasks.clear();
 
             if(zwitch.developer_output())
@@ -251,6 +286,8 @@ namespace zefDB {
             if(zwitch.developer_output())
                 std::cerr << "Finished stopping butler" << std::endl;
             butler.reset();
+
+            _running = false;
         }
 
 
@@ -1298,7 +1335,7 @@ namespace zefDB {
                                   return me.please_stop
                                       || (network.connected && me.gd->should_sync
                                           && (me.gd->sync_head == 0 || (me.gd->sync_head < me.gd->read_head.load())))
-                                      || (!zwitch.write_thread_runs_subscriptions() && me.gd->latest_complete_tx > me.gd->manager_tx_head.load());
+                                      || (me.gd->latest_complete_tx > me.gd->manager_tx_head.load());
                               });
                                   
                     if(me.please_stop)
@@ -1308,46 +1345,43 @@ namespace zefDB {
                     // not set. TODO: In the future, probably indicate the
                     // difference between "anytime" subs and "in reaction to
                     // updates" subs.
-                    if(!me.gd->is_primary_instance ||
-                       !zwitch.write_thread_runs_subscriptions()) {
-                        blob_index target_complete_tx = me.gd->latest_complete_tx;
-                        while(!me.please_stop
-                              && target_complete_tx > me.gd->manager_tx_head) {
-                            // Do one tx at a time until we have nothing left to do.
-
-                            // First check for subscriptions
-                            EZefRef last_tx{me.gd->manager_tx_head, *me.gd};
-                            if(!(last_tx | has_out[BT.NEXT_TX_EDGE])) {
-                                std::cerr << "guid: " << me.uid << std::endl;
-                                std::cerr << "write_head: " << me.gd->write_head.load() << std::endl;
-                                std::cerr << "read_head: " << me.gd->read_head.load() << std::endl;
-                                std::cerr << "latest_complete_tx: " << me.gd->latest_complete_tx.load() << std::endl;
-                                std::cerr << "manager_tx_head: " << me.gd->manager_tx_head.load() << std::endl;
-                                throw std::runtime_error("Big problem in sync thread, could not find BT.NEXT_TX_EDGE from last_tx");
-                            }
-                            EZefRef this_tx = last_tx >> BT.NEXT_TX_EDGE;
-
-                            try {
-                                run_subscriptions(*me.gd, this_tx);
-                            } catch(...) {
-                                std::cerr << "There was a failure in the run_subscriptions part of the sync thread." << std::endl;
-                                throw;
-                            }
+                    blob_index target_complete_tx = me.gd->latest_complete_tx;
+                    while(!me.please_stop
+                            && target_complete_tx > me.gd->manager_tx_head) {
+                        // Do one tx at a time until we have nothing left to do.
+
+                        // First check for subscriptions
+                        EZefRef last_tx{me.gd->manager_tx_head, *me.gd};
+                        if(!(last_tx | has_out[BT.NEXT_TX_EDGE])) {
+                            std::cerr << "guid: " << me.uid << std::endl;
+                            std::cerr << "write_head: " << me.gd->write_head.load() << std::endl;
+                            std::cerr << "read_head: " << me.gd->read_head.load() << std::endl;
+                            std::cerr << "latest_complete_tx: " << me.gd->latest_complete_tx.load() << std::endl;
+                            std::cerr << "manager_tx_head: " << me.gd->manager_tx_head.load() << std::endl;
+                            throw std::runtime_error("Big problem in sync thread, could not find BT.NEXT_TX_EDGE from last_tx");
+                        }
+                        EZefRef this_tx = last_tx >> BT.NEXT_TX_EDGE;
 
-                            // TODO:
-                            // TODO:
-                            // TODO:
-                            // TODO: We probably need to save off this
-                            // manager_tx_head, so that premature exits can be
-                            // picked up from later. This is especially relevant
-                            // for the tokens.
-                            // TODO:
-                            // TODO:
-                            // TODO:
-                            update(me.gd->heads_locker, me.gd->manager_tx_head, index(this_tx));
+                        try {
+                            run_subscriptions(*me.gd, this_tx);
+                        } catch(...) {
+                            std::cerr << "There was a failure in the run_subscriptions part of the sync thread." << std::endl;
+                            throw;
                         }
-                        internals::execute_queued_fcts(*me.gd);
+
+                        // TODO:
+                        // TODO:
+                        // TODO:
+                        // TODO: We probably need to save off this
+                        // manager_tx_head, so that premature exits can be
+                        // picked up from later. This is especially relevant
+                        // for the tokens.
+                        // TODO:
+                        // TODO:
+                        // TODO:
+                        update(me.gd->heads_locker, me.gd->manager_tx_head, index(this_tx));
                     }
+                    internals::execute_queued_fcts(*me.gd);
 
                     if(me.gd->error_state != GraphData::ErrorState::OK)
                         throw std::runtime_error("Sync worker for graph detected invalid state and is aborting");
@@ -1460,7 +1494,11 @@ namespace zefDB {
             if(gtd->gd != nullptr) {
                 // This doesn't need to be synchronised, as only this thread cares about it. It is only used to "disable" ~Graph inside of this function.
                 // If keep_alive, then get rid of that now
+                if(zwitch.developer_output())
+                    std::cerr << "About to reset the keep alive" << std::endl;
                 gtd->keep_alive_g.reset();
+                if(zwitch.developer_output())
+                    std::cerr << "Reset the keep alive" << std::endl;
                 gtd->gd->started_destructing = true;
                 if(!gtd->queue._closed)
                     gtd->queue.set_closed();
@@ -1502,7 +1540,9 @@ namespace zefDB {
                     std::cerr << "Unloading graph: " << gtd->uid << std::endl;
                     // std::cerr << "Heads: sync: " << gtd->gd->sync_head.load() << ", read: " << gtd->gd->read_head.load() << ", write: " << gtd->gd->write_head.load() << std::endl;
                 }
+                gtd->debug_last_action = "Before destructing GraphData";
                 gtd->gd->~GraphData();
+                gtd->debug_last_action = "Destructed GraphData";
                 MMap::destroy_mmap((void*)gtd->gd);
                 gtd->debug_last_action = "Cleaned up mmap";
             }
@@ -1535,30 +1575,6 @@ namespace zefDB {
         ////////////////////////////////////////////////
         // * Connection management
 
-        std::string get_auto_connect() {
-            // if(!initialised_python_core) {
-            //     std::cerr << "WARNING!!!" << std::endl;
-            //     std::cerr << "WARNING!!!" << std::endl;
-            //     std::cerr << "WARNING!!!" << std::endl;
-            //     std::cerr << "WARNING!!!" << std::endl;
-            //     std::cerr << "WARNING!!!" << std::endl;
-            //     std::cerr << "C library wants to call into python module before it has finished loading. Specifically, it wants to determine the \"login.autoConnect\" value. Going to return \"auto\" to avoid creating a circular dependency." << std::endl;
-            //     std::cerr << "WARNING!!!" << std::endl;
-            //     std::cerr << "WARNING!!!" << std::endl;
-            //     std::cerr << "WARNING!!!" << std::endl;
-            //     std::cerr << "WARNING!!!" << std::endl;
-            //     std::cerr << "WARNING!!!" << std::endl;
-            //     return "auto";
-            // }
-                
-            // py::gil_scoped_acquire acquire;
-            // auto get_config = py::module_::import("zef.ops._config").attr("get_config");
-            // return get_config("login.autoConnect").cast<std::string>();
-
-            // TODO: reimplement config into C
-            return "auto";
-        }
-
         bool Butler::want_upstream_connection() {
             if(network.uri == "")
                 return false;
@@ -1567,7 +1583,7 @@ namespace zefDB {
             if(network.is_running())
                 return true;
 
-            std::string auto_connect = get_auto_connect();
+            std::string auto_connect = std::get<std::string>(get_config_var("login.autoConnect"));
 
             if(auto_connect == "auto" && have_auth_credentials())
                 return true;
@@ -1641,6 +1657,52 @@ namespace zefDB {
             }
         }
 
+        std::string Butler::who_am_i() {
+            std::string name;
+
+            std::optional<std::string> key_string = load_forced_zefhub_key();
+            if(key_string) {
+                if(*key_string == constants::zefhub_guest_key) {
+                    name = "GUEST via forced key";
+                } else {
+                    // This is  duplicated with get_firebase_refresh_token_email which is annoying
+                    int colon = key_string->find(':');
+                    name = key_string->substr(0,colon) + " via forced key";
+                }
+            } else if(!have_auth_credentials()) {
+                name = "";
+            } else {
+                if(have_logged_in_as_guest) {
+                    name = "GUEST via login prompt";
+                } else {
+                    auto credentials_file = zefdb_config_path() / "credentials";
+                    if(!std::filesystem::exists(credentials_file)) {
+                        name = "";
+                    } else {
+                        std::ifstream file(credentials_file);
+                        json j = json::parse(file);
+                        refresh_token = j["refresh_token"].get<std::string>();
+                        std::string token = get_firebase_token_refresh_token(refresh_token);
+                        using traits = jwt::traits::nlohmann_json;
+                        auto decoded = jwt::decode<traits>(token);
+                        name = decoded.get_payload_claim("email").to_json();
+
+                        auto firebase_field = decoded.get_payload_claim("firebase").to_json();
+                        if(firebase_field.contains("sign_in_provider"))
+                            name += " through firebase provider: " + firebase_field["sign_in_provider"].get<std::string>();
+                    }
+                }
+            }
+
+            if(connection_authed)
+                name += " (CONNECTED)";
+            else if(fatal_connection_error)
+                name += " (CONNECITON ERROR)";
+            else
+                name += " (DISCONNECTED)";
+            return name;
+        }
+
         Butler::header_list_t Butler::prepare_send_headers(void) {
             determine_refresh_token();
             decltype(network)::header_list_t headers;
@@ -1666,8 +1728,7 @@ namespace zefDB {
             if(network.is_running())
                 return;
 
-            // TODO: Look this up
-            std::string auto_connect = get_auto_connect();
+            std::string auto_connect = std::get<std::string>(get_config_var("login.autoConnect"));
             if(have_auth_credentials() || auto_connect == "always")
                 // Note: in the case of already having credentials,
                 // ensure_auth_credentials, will make sure the credentials are
@@ -1764,20 +1825,6 @@ namespace zefDB {
             return name;
         }
 
-        std::string get_default_zefhub_uri() {
-            char * env = std::getenv("ZEFHUB_URL");
-            if (env != nullptr)
-                return std::string(env);
-
-            // TODO: Load from file
-            
-            // return "http://localhost:5001";
-            // return "wss://hub.zefhub.com";
-            // return "wss://hubv2.zefhub.com";
-            return "wss://hub.zefhub.io";
-        }
-
-
         //////////////////////////////
         // * Credentials
 
@@ -1800,7 +1847,7 @@ namespace zefDB {
             }
 
             // Old location for fallback
-#ifdef _MSC_VER
+#ifdef ZEF_WIN32
             env = std::getenv("LOCALAPPDATA");
 #else
             env = std::getenv("HOME");
@@ -2011,13 +2058,7 @@ namespace zefDB {
             if (env != nullptr)
                 return std::filesystem::path(std::string(env)) / upstream_name;
 
-#ifdef _MSC_VER
-            char * home = std::getenv("LOCALAPPDATA");
-#else
-            char * home = std::getenv("HOME");
-#endif
-            std::filesystem::path path(home);
-            path /= ".zef";
+            std::filesystem::path path = zefdb_config_path();
             path /= "graphs";
             path /= upstream_name;
             return path;
@@ -2056,7 +2097,7 @@ namespace zefDB {
             if (env != nullptr)
                 return std::string(env);
 
-#ifdef _MSC_VER
+#ifdef ZEF_WIN32
             env = std::getenv("LOCALAPPDATA");
 #else
             env = std::getenv("HOME");
@@ -2075,6 +2116,7 @@ namespace zefDB {
         // * External handlers
 
 
+        // ** Merge handler
         std::optional<std::function<merge_handler_t>> merge_handler;
         json pass_to_merge_handler(Graph g, const json & payload) {
             if(!merge_handler)
@@ -2094,5 +2136,24 @@ namespace zefDB {
                 std::cerr << "Warning, no merge_handler registered to be removed." << std::endl;
             merge_handler.reset();
         }
+
+        // ** Schema validator
+        std::optional<std::function<schema_validator_t>> schema_validator;
+        void pass_to_schema_validator(ZefRef tx) {
+            if(schema_validator)
+                (*schema_validator)(tx);
+        }
+
+        void register_schema_validator(std::function<schema_validator_t> func) {
+            if(schema_validator)
+                throw std::runtime_error("schema_validator has already been registered.");
+            schema_validator = func;
+        }
+
+        void remove_schema_validator() {
+            if(!schema_validator)
+                std::cerr << "Warning, no schema_validator registered to be removed." << std::endl;
+            schema_validator.reset();
+        }
     }
 }
diff --git a/core/src/butler/butler_handlers_graph_manager.cpp b/core/src/butler/butler_handlers_graph_manager.cpp
index 0da41758..5f2e1820 100644
--- a/core/src/butler/butler_handlers_graph_manager.cpp
+++ b/core/src/butler/butler_handlers_graph_manager.cpp
@@ -179,7 +179,7 @@ int resolve_memory_style(int mem_style, bool synced) {
     }
     if(mem_style == MMap::MMAP_STYLE_FILE_BACKED) {
         // TODO: Check if we have filesystem access here.
-#ifdef _MSC_VER
+#ifdef ZEF_WIN32
         // Until support is there, need to force anonymous.
         mem_style = MMap::MMAP_STYLE_ANONYMOUS;
 #endif
@@ -206,7 +206,7 @@ void apply_update_with_caches(GraphData & gd, const UpdatePayload & payload, boo
     // Apply blob updates
 
     internals::include_new_blobs(gd, heads.blobs.from, heads.blobs.to, blob_bytes, double_link, false);
-    gd.latest_complete_tx = index(internals::get_latest_complete_tx_node(gd));
+    update(gd.heads_locker, gd.latest_complete_tx, index(internals::get_latest_complete_tx_node(gd)));
 
     // Apply cache updates
 
@@ -308,6 +308,13 @@ void Butler::graph_worker_handle_message(Butler::GraphTrackingData & me, NewGrap
             apply_update_with_caches(*me.gd, *content.payload, false, false);
             me.gd->manager_tx_head = me.gd->latest_complete_tx.load();
             // Note: don't set sync_head here, it should remain at 0.
+        } else {
+            // create a first transaction on a new graph. This layout allows us to assume that there always is a tx,
+            // which helps in various high level functions.
+            //
+            // Note: this is done here instead of in the GraphData constructor
+            // as creating a transaction will trigger the sync thread.
+            auto my_tx = Transaction(*me.gd, false, false);
         }
 
         if(!content.internal_use_only) {
@@ -856,7 +863,7 @@ void Butler::graph_worker_handle_message(Butler::GraphTrackingData & me, MergeRe
     Graph target_g(*me.gd);
 
     // Do different things based on the payload.
-    std::visit(overload {
+    std::visit(overloaded {
             [&](MergeRequest::PayloadGraphDelta & payload) {
                 try {
                     // py::gil_scoped_acquire acquire;
diff --git a/core/src/butler/communication.cpp b/core/src/butler/communication.cpp
index 8bb2719e..60ebd493 100644
--- a/core/src/butler/communication.cpp
+++ b/core/src/butler/communication.cpp
@@ -15,7 +15,7 @@
 #include "butler/communication.h"
 #include "zwitch.h"
 
-#ifdef _MSC_VER
+#ifdef ZEF_WIN32
 #include <wincrypt.h>
 #endif
 
@@ -125,9 +125,10 @@ namespace zefDB {
 
 
         void PersistentConnection::fail_handler(websocketpp::connection_hdl hdl) {
-#if ZEFDB_ALLOW_NO_TLS
-            std::visit([this](auto & con) {
-#endif
+            visit_endpoint([this,&hdl](auto & endpoint) {
+                auto con = endpoint->get_con_from_hdl(hdl);
+                if(!con)
+                    return;
                 auto ec = con->get_ec();
                 int response_code = con->get_response_code();
                 if(response_code == 401) {
@@ -148,9 +149,7 @@ namespace zefDB {
                 });
                 if (outside_close_handler)
                     outside_close_handler(true);
-#if ZEFDB_ALLOW_NO_TLS
-            }, con);
-#endif
+            });
         }
         void PersistentConnection::pong_timeout_handler(websocketpp::connection_hdl hdl, std::string s) {
             if(zwitch.zefhub_communication_output()) {
@@ -177,6 +176,18 @@ namespace zefDB {
             debug_time_print("start of open_handler");
             update(locker, connected, true);
             last_connect_time = std::chrono::steady_clock::now();
+            if(should_stop) {
+                visit_endpoint([this,&hdl](auto & endpoint) {
+                    auto con = endpoint->get_con_from_hdl(hdl);
+                    if(con) {
+                        std::error_code ec;
+                        con->close(websocketpp::close::status::going_away, "", ec);
+                        if(ec) 
+                            std::cout << "> Closing connection error (in open_handler): " << ec.message() << std::endl;
+                    }
+                });
+                return;
+            }
             send_ping();
 
             if (outside_open_handler) {
@@ -184,11 +195,9 @@ namespace zefDB {
             }
         };
         void PersistentConnection::close_handler(websocketpp::connection_hdl hdl) {
-                // std::cerr << "Closed" << std::endl;
-#if ZEFDB_ALLOW_NO_TLS
-            std::visit([this](auto & con) {
-#endif
-            if(con) {
+            visit_endpoint([this,&hdl](auto & endpoint) {
+                auto con = endpoint->get_con_from_hdl(hdl);
+                if(con) {
                     auto ec = con->get_ec();
                     if(ec) {
                         if(zwitch.developer_output()) {
@@ -198,16 +207,20 @@ namespace zefDB {
                         last_was_failure = true;
                     }
                 }
-            con.reset();
-            update(locker, [&]() {
-                connected = false;
-                wspp_in_control = false;
+                update(locker, [&]() {
+                    connected = false;
+                    wspp_in_control = false;
+                    // This is getting the shared pointer on our object, not
+                    // that of websocketpp. This might not be necessary if we
+                    // make sure to use connected instead of _con to determine
+                    // connection state.
+                    std::visit([&](auto & con) {
+                        con.reset();
+                    }, _con);
+                });
+                if (outside_close_handler)
+                    outside_close_handler(false);
             });
-            if (outside_close_handler)
-                outside_close_handler(false);
-#if ZEFDB_ALLOW_NO_TLS
-            }, con);
-#endif
         }
         void PersistentConnection::message_handler_tls(websocketpp::connection_hdl hdl, base_client_tls_t::message_ptr msg) {
                 outside_message_handler(msg->get_payload());
@@ -217,7 +230,7 @@ namespace zefDB {
                 outside_message_handler(msg->get_payload());
             }
 
-#ifdef _MSC_VER
+#ifdef ZEF_WIN32
         void add_windows_root_certs(ssl_context_ptr & ctx) {
             HCERTSTORE hStore = CertOpenSystemStore(0, "ROOT");
             if(hStore == NULL)
@@ -253,7 +266,7 @@ namespace zefDB {
                                  asio::ssl::context::single_dh_use);
 
 
-#ifdef _MSC_VER
+#ifdef ZEF_WIN32
                 add_windows_root_certs(ctx);
 #else
                 ctx->set_default_verify_paths();
@@ -289,33 +302,34 @@ namespace zefDB {
                 if(uri.find("ws://") == 0) {
                     // endpoint.emplace<client_notls_t>();
                     endpoint = std::make_shared<base_client_notls_t>();
+                    _con = base_client_notls_t::connection_ptr(nullptr);
                     if(zwitch.zefhub_communication_output())
                         std::cerr << "Using no TLS" << std::endl;
                 } else if(uri.find("wss://") == 0) {
                     // endpoint.emplace<client_tls_t>();
                     endpoint = std::make_shared<base_client_tls_t>();
+                    _con = base_client_tls_t::connection_ptr(nullptr);
                     if(zwitch.zefhub_communication_output())
                         std::cerr << "Using TLS" << std::endl;
                 } else {
                     throw std::runtime_error("Unknown protocol for uri: " + uri);
                 }
                 
-                std::visit([this](auto & endpoint) {
 #else
                 endpoint = std::make_shared<base_client_t>();
 #endif
-                endpoint->clear_access_channels(websocketpp::log::alevel::all);
-                endpoint->clear_error_channels(websocketpp::log::elevel::all);
-                endpoint->init_asio();
-                endpoint->start_perpetual();
-                endpoint->set_fail_handler(std::bind(&PersistentConnection::fail_handler, this, std::placeholders::_1));
-                endpoint->set_pong_timeout_handler(std::bind(&PersistentConnection::pong_timeout_handler, this, std::placeholders::_1, std::placeholders::_2));
-                endpoint->set_pong_handler(std::bind(&PersistentConnection::pong_handler, this, std::placeholders::_1, std::placeholders::_2));
-                endpoint->set_open_handler(std::bind(&PersistentConnection::open_handler, this, std::placeholders::_1));
-                endpoint->set_close_handler(std::bind(&PersistentConnection::close_handler, this, std::placeholders::_1));
-#if ZEFDB_ALLOW_NO_TLS
-                }, endpoint);
-#endif
+
+                visit_endpoint([this](auto & endpoint) {
+                    endpoint->clear_access_channels(websocketpp::log::alevel::all);
+                    endpoint->clear_error_channels(websocketpp::log::elevel::all);
+                    endpoint->init_asio();
+                    endpoint->start_perpetual();
+                    endpoint->set_fail_handler(std::bind(&PersistentConnection::fail_handler, this, std::placeholders::_1));
+                    endpoint->set_pong_timeout_handler(std::bind(&PersistentConnection::pong_timeout_handler, this, std::placeholders::_1, std::placeholders::_2));
+                    endpoint->set_pong_handler(std::bind(&PersistentConnection::pong_handler, this, std::placeholders::_1, std::placeholders::_2));
+                    endpoint->set_open_handler(std::bind(&PersistentConnection::open_handler, this, std::placeholders::_1));
+                    endpoint->set_close_handler(std::bind(&PersistentConnection::close_handler, this, std::placeholders::_1));
+                });
 
 #if ZEFDB_ALLOW_NO_TLS
                 std::visit(overloaded{
@@ -329,13 +343,11 @@ namespace zefDB {
                             endpoint->set_message_handler(std::bind(&PersistentConnection::message_handler_notls, this, std::placeholders::_1, std::placeholders::_2));
                         }
                     }, endpoint);
-
-                std::visit([this](auto & endpoint) {
 #endif
+
+                visit_endpoint([this](auto & endpoint) {
                     ws_thread = std::make_unique<std::thread>(&std::remove_reference<decltype(endpoint)>::type::element_type::run, endpoint.get());
-#if ZEFDB_ALLOW_NO_TLS
-                }, endpoint);
-#endif
+                });
             } catch(const std::exception & exc) {
                 std::cerr << "Bad error while trying to setup the websocket++ endpoint:" << exc.what() << std::endl;
                 return;
@@ -346,75 +358,82 @@ namespace zefDB {
             create_endpoint();
 
             connected = false;
+            visit_endpoint([this](auto & endpoint) {
+                std::error_code ec;
+                auto con = endpoint->get_connection(uri, ec);
+                if (ec) {
+                    std::cout << "> Connect initialization error: " << ec.message() << std::endl;
+                    throw std::runtime_error("Couldn't create connection for websocket++"); 
+                }
+                if(prepare_headers_func)
+                    for(auto & item : (*prepare_headers_func)())
+                        con->append_header(item.first, item.second);
+
+                debug_time_print("before endpoint connect");
+                endpoint->connect(con);
+                // This is a little tricky - we should probably have put the
+                // endpoint and the connection in the same variant to be able to
+                // access this with type guarantees. As it is, nesting std::visits
+                // will form the 2x2 product of all type combinations.
 #if ZEFDB_ALLOW_NO_TLS
-            std::visit([this](auto & endpoint) {
-#endif
-            std::error_code ec;
-            auto con = endpoint->get_connection(uri, ec);
-            if (ec) {
-                std::cout << "> Connect initialization error: " << ec.message() << std::endl;
-                throw std::runtime_error("Couldn't create connection for websocket++"); 
-            }
-            if(prepare_headers_func)
-                for(auto & item : (*prepare_headers_func)())
-                    con->append_header(item.first, item.second);
-
-            debug_time_print("before endpoint connect");
-            // This is a case that can be hit in quick scripts, and if left out
-            // will still allow the client to exit, but very slowly. I'm not
-            // sure what takes so long, it seems like a timeout but I don't know
-            // where it is. This check here can't hurt and seems reasonable.
-            if(should_stop)
-                return;
-            endpoint->connect(con);
-            this->con = con;
-#if ZEFDB_ALLOW_NO_TLS
-            }, endpoint);
+                // This type manipulation needs an example. If the
+                // visit_endpoint above is called with auto&=client_tls_t&, then
+                // this type manipulation will get us
+                // base_client_tls_t::connection_ptr
+                auto & ptr_con = std::get<typename std::remove_reference_t<decltype(endpoint)>::element_type::connection_ptr>(_con);
+#else
+                auto & ptr_con = _con;
 #endif
+                update(locker, [&]() {
+                    if(should_stop) {
+                        con->close(websocketpp::close::status::going_away, "", ec);
+                        if(ec) 
+                            std::cout << "> Closing connection error (in start_connection): " << ec.message() << std::endl;
+                    } else {
+                        atomic_store(&ptr_con, con);
+                    }
+                });
+            });
         }
 
         void PersistentConnection::close(bool failure) {
-#if ZEFDB_ALLOW_NO_TLS
-            std::visit([this,failure](auto & con) {
-#endif
+            update(locker, [&]() {
                 should_stop = true;
-                if(con) {
-                    std::error_code ec;
-                    con->close(websocketpp::close::status::going_away, "", ec);
-                    con.reset();
-                }
+                std::visit([&](auto & con) {
+                    if(con) {
+                        std::error_code ec;
+                        con->close(websocketpp::close::status::going_away, "", ec);
+                        if(ec)
+                            std::cout << "> Closing connection error: " << ec.message() << std::endl;
+                        con.reset();
+                    }
+                }, _con);
 
-                update(locker, [&]() {
-                    connected = false;
-                    wspp_in_control = false;
-                    // Note: don't reset this to false, because the user might call
-                    // this after a real failure that they didn't know about.
-                    if (failure)
-                        last_was_failure = failure;
-                });
-#if ZEFDB_ALLOW_NO_TLS
-            }, con);
-#endif
-            }
+                connected = false;
+                wspp_in_control = false;
+                // Note: don't reset this to false, because the user might call
+                // this after a real failure that they didn't know about.
+                if (failure)
+                    last_was_failure = failure;
+            });
+        }
         void PersistentConnection::restart(bool failure) {
-#if ZEFDB_ALLOW_NO_TLS
-            std::visit([&](auto & con) {
-#endif
+            visit_con([&](auto & con) {
                 if(!con)
                     return;
 
                 std::error_code ec;
                 con->close(websocketpp::close::status::going_away, "", ec);
+                if(ec)
+                    std::cout << "> Closing connection error (in restart): " << ec.message() << std::endl;
                 con.reset();
                 update(locker, [&]() {
                     connected = false;
                     wspp_in_control = false;
                     last_was_failure = failure;
                 });
-#if ZEFDB_ALLOW_NO_TLS
-            }, con);
-#endif
-            }
+            });
+        }
 
             //////////////////////////////////////////
             // * Utility extensions
@@ -435,9 +454,7 @@ namespace zefDB {
                 if(should_stop)
                     return;
 
-#if ZEFDB_ALLOW_NO_TLS
-                std::visit([this,&msg,&opcode](auto & con) {
-#endif
+                visit_con([this,&msg,&opcode](auto & con) {
                 if(con) {
                             std::error_code ec = con->send(msg, opcode);
                             if (ec) {
@@ -450,9 +467,7 @@ namespace zefDB {
                                 }
                             }
                 }
-#if ZEFDB_ALLOW_NO_TLS
-            }, con);
-#endif
+                });
             }
 
         void PersistentConnection::start_running() {
@@ -472,9 +487,9 @@ namespace zefDB {
 
 
         void PersistentConnection::send_ping() {
-#if ZEFDB_ALLOW_NO_TLS
-            std::visit([this](auto & con) {
-#endif
+            visit_con([this](auto & con) {
+                if(!con)
+                    return;
                 auto duration = std::chrono::steady_clock::now().time_since_epoch();
                 long long now = std::chrono::duration_cast<std::chrono::milliseconds>(duration).count();
                 std::error_code ec;
@@ -483,15 +498,13 @@ namespace zefDB {
                     std::cerr << "Error sending ping: " << ec.message() << std::endl;
                     // throw std::runtime_error("Error sending ping: " + ec.message());
                 }
-#if ZEFDB_ALLOW_NO_TLS
-            }, con);
-#endif
+            });
         }
 
         void PersistentConnection::manager_runner() {
             try {
                 while(true) {
-                    wait_same(locker, wspp_in_control, false, ping_interval);
+                    wait_pred(locker, [this]() { return !wspp_in_control || should_stop; });
 
                     if(should_stop)
                         break;
@@ -499,15 +512,7 @@ namespace zefDB {
                     // This is how we test whether we should ping, or if we've
                     // actually lost the connection and need to reconnect.
                     if(wspp_in_control) {
-#if ZEFDB_ALLOW_NO_TLS
-                        std::visit([this](auto & con) {
-#endif
-                            if(con) {
-                                send_ping();
-                            }
-#if ZEFDB_ALLOW_NO_TLS
-                        }, con);
-#endif
+                        send_ping();
                         continue;
                     }
 
@@ -527,17 +532,14 @@ namespace zefDB {
                     }
                     // std::cerr << "Trying to connect" << std::endl;
 
-                    wspp_in_control = true;
+                    update(locker, wspp_in_control, true);
                     start_connection();
                 }
 
-#if ZEFDB_ALLOW_NO_TLS
-                std::visit([this](auto & endpoint) {
-#endif
-                    endpoint->stop_perpetual();
-#if ZEFDB_ALLOW_NO_TLS
-                }, endpoint);
-#endif
+                visit_endpoint([this](auto & endpoint) {
+                    if(endpoint)
+                        endpoint->stop_perpetual();
+                });
                 // We may have created a new connection in the intermediate time
                 // between this and the last close. So do it again now. (the
                 // last time, con may have been empty, if start_connection was
@@ -553,13 +555,9 @@ namespace zefDB {
                 }
 
                 should_stop = true;
-#if ZEFDB_ALLOW_NO_TLS
-                std::visit([this](auto & endpoint) {
-#endif
+                visit_endpoint([this](auto & endpoint) {
                     endpoint->stop_perpetual();
-#if ZEFDB_ALLOW_NO_TLS
-                }, endpoint);
-#endif
+                });
                 ws_thread->join();
                 ws_thread.reset();
             }
diff --git a/core/src/from_json.cpp b/core/src/from_json.cpp
index bd15becd..c22bb633 100644
--- a/core/src/from_json.cpp
+++ b/core/src/from_json.cpp
@@ -268,7 +268,10 @@ namespace zefDB {
                     cur_index += blob_index_size(uzr);
                 }
 
-                verification::verify_graph_double_linking(g);
+                if(!verification::verify_graph_double_linking(g)
+                   || !verification::verify_chronological_instantiation_order(g)) {
+                    throw std::runtime_error("Verificaiton failed after rebuilding graph");
+                }
             }
             // Now use this byte data to construct a new graph which will do
             // apply blobs, create the caches and make it managed by the butler.
diff --git a/core/src/graph.cpp b/core/src/graph.cpp
index b00077b2..6c8e6442 100644
--- a/core/src/graph.cpp
+++ b/core/src/graph.cpp
@@ -299,19 +299,17 @@ namespace zefDB {
         if(!response.generic.success)
             throw std::runtime_error("Unable to create new graph: " + response.generic.reason);
         *this = std::move(*response.g);
-        if(internal_use_only)
+        if(internal_use_only) {
+            std::cerr << "Creating graph for internal use only - this shouldn't be happening anymore!" << std::endl;
+            std::cerr << "Creating graph for internal use only - this shouldn't be happening anymore!" << std::endl;
+            std::cerr << "Creating graph for internal use only - this shouldn't be happening anymore!" << std::endl;
+            std::cerr << "Creating graph for internal use only - this shouldn't be happening anymore!" << std::endl;
             return;
+        }
         // std::cerr << "New graph Graph(), ref_count = " << my_graph_data().reference_count.load() << std::endl;
         
         // After here, we should always destruct
         try {
-            // create a first transaction on a new graph. This layout allows us to assume that there always is a tx,
-            // which helps in various high level functions.
-            //
-            // Note: this is done here instead of in the GraphData constructor
-            // as creating a transaction will trigger the sync thread.
-            { auto my_tx = Transaction(*this); }
-
             if(sync)
                 this->sync();
         } catch(...) {
@@ -634,66 +632,70 @@ namespace zefDB {
         // However, the contract is that other threads should only read and use
         // blobs/edges up to read_head anyway so this would be redundant.
 
-        // TODO: Change this to be the inverse of apply_double_linking
-        std::unordered_set<blob_index> updated_last_blobs;
+        if(false) {
+            // TODO: Change this to be the inverse of apply_double_linking
+            std::unordered_set<blob_index> updated_last_blobs;
+
+            cur_index = constants::ROOT_NODE_blob_index;
+            while(cur_index < index_hi) {
+                EZefRef ezr{cur_index, gd};
+                int this_size = size_of_blob(ezr);
+                int new_last_blob = -1;
+                bool is_start_of_edges = false;
+                if(internals::has_edge_list(ezr)) {
+                    visit_blob_with_edges([&](auto & x) {
+                        int this_last_blob_offset = -1;
+                        for(int i = 0; i < x.edges.local_capacity ; i++) {
+                            if(abs(x.edges.indices[i]) >= index_hi) {
+                                x.edges.indices[i] = 0;
+                                if(this_last_blob_offset == -1)
+                                    this_last_blob_offset = i;
+                            }
+                        }
 
-        cur_index = constants::ROOT_NODE_blob_index;
-        while(cur_index < index_hi) {
-            EZefRef ezr{cur_index, gd};
-            int this_size = size_of_blob(ezr);
-            int new_last_blob = -1;
-            bool is_start_of_edges = false;
-            if(internals::has_edge_list(ezr)) {
-                visit_blob_with_edges([&](auto & x) {
-                    int this_last_blob_offset = -1;
-                    for(int i = 0; i < x.edges.local_capacity ; i++) {
-                        if(abs(x.edges.indices[i]) >= index_hi) {
-                            x.edges.indices[i] = 0;
+                        if(x.edges.indices[x.edges.local_capacity] >= index_hi) {
+                            x.edges.indices[x.edges.local_capacity] = blobs_ns::sentinel_subsequent_index;
                             if(this_last_blob_offset == -1)
-                                this_last_blob_offset = i;
+                                this_last_blob_offset = x.edges.local_capacity;
                         }
-                    }
-
-                    if(x.edges.indices[x.edges.local_capacity] >= index_hi) {
-                        x.edges.indices[x.edges.local_capacity] = blobs_ns::sentinel_subsequent_index;
-                        if(this_last_blob_offset == -1)
-                            this_last_blob_offset = x.edges.local_capacity;
-                    }
 
-                    if(this_last_blob_offset != -1) {
-                        if(this_last_blob_offset == 0)
-                            new_last_blob = 0;
-                        else {
-                            uintptr_t direct_ptr = (uintptr_t)&x.edges.indices[this_last_blob_offset];
-                            blob_index * ptr = (blob_index*)(direct_ptr - (direct_ptr % constants::blob_indx_step_in_bytes));
-                            new_last_blob = blob_index_from_ptr(ptr);
+                        if(this_last_blob_offset != -1) {
+                            if(this_last_blob_offset == 0)
+                                new_last_blob = 0;
+                            else {
+                                uintptr_t direct_ptr = (uintptr_t)&x.edges.indices[this_last_blob_offset];
+                                blob_index * ptr = (blob_index*)(direct_ptr - (direct_ptr % constants::blob_indx_step_in_bytes));
+                                new_last_blob = blob_index_from_ptr(ptr);
+                            }
                         }
-                    }
-                }, ezr);
-
-                if(new_last_blob != -1) {
-                    if(get<BlobType>(ezr) == BlobType::DEFERRED_EDGE_LIST_NODE) {
-                        auto def = (blobs_ns::DEFERRED_EDGE_LIST_NODE*)ezr.blob_ptr;
-                        // We only update if the source blob wasn't previously updated.
-                        if(updated_last_blobs.count(def->first_blob) == 0) {
-                            EZefRef orig_ezr{def->first_blob, gd};
-                            *internals::last_edge_holding_blob(orig_ezr) = new_last_blob;
-                            updated_last_blobs.insert(def->first_blob);
+                    }, ezr);
+
+                    if(new_last_blob != -1) {
+                        if(get<BlobType>(ezr) == BlobType::DEFERRED_EDGE_LIST_NODE) {
+                            auto def = (blobs_ns::DEFERRED_EDGE_LIST_NODE*)ezr.blob_ptr;
+                            // We only update if the source blob wasn't previously updated.
+                            if(updated_last_blobs.count(def->first_blob) == 0) {
+                                EZefRef orig_ezr{def->first_blob, gd};
+                                *internals::last_edge_holding_blob(orig_ezr) = new_last_blob;
+                                updated_last_blobs.insert(def->first_blob);
+                            } else {
+                                // If we get here something is weird - we are
+                                // removing items from a deferred edge list that
+                                // *must* be beyond the end of the index_hi.
+                                throw std::runtime_error("We should never get here!");
+                            }
                         } else {
-                            // If we get here something is weird - we are
-                            // removing items from a deferred edge list that
-                            // *must* be beyond the end of the index_hi.
-                            throw std::runtime_error("We should never get here!");
+                            *internals::last_edge_holding_blob(ezr) = new_last_blob;
                         }
-                    } else {
-                        *internals::last_edge_holding_blob(ezr) = new_last_blob;
-                    }
 
-                    updated_last_blobs.insert(cur_index);
+                        updated_last_blobs.insert(cur_index);
+                    }
                 }
-            }
 
-            cur_index += blob_index_size(ezr);
+                cur_index += blob_index_size(ezr);
+            }
+        } else {
+            internals::undo_double_linking(gd, index_hi, gd.write_head.load());
         }
 
         // Now blank out the memory above
@@ -822,40 +824,31 @@ namespace zefDB {
 
         // Mutually exclusive access to transactions, based on thread id.
         // std::thread::id() works as the "unset" value in this case.
-        if(zwitch.write_thread_runs_subscriptions()) {
-            // Any thread can take this over
-            auto this_id = std::this_thread::get_id();
+        auto this_id = std::this_thread::get_id();
+        if(this_id == gd.sync_thread_id) {
+            // If we are here, then we are the manager running subscriptions. We
+            // only need to take the write role.
             update_when_ready(gd.open_tx_thread_locker,
                             gd.open_tx_thread,
                             std::thread::id(),
                             this_id);
         } else {
-            auto this_id = std::this_thread::get_id();
-            if(this_id == gd.sync_thread_id) {
-                // If we are here, then we are the manager running subscriptions. We
-                // only need to take the write role.
+            // We are a client, we need to wait for the manager to have caught
+            // up and no-one else is writing.
+
+            // Note: the two things we check are actually accessed using
+            // different locks. This is a little weird... and might be
+            // problematic?
+            while(gd.latest_complete_tx != gd.manager_tx_head.load()
+                || gd.open_tx_thread != this_id) {
+                // Give up the open_tx_thread if we stole it before the manager can catch up.
+                if(gd.open_tx_thread == this_id)
+                    update(gd.open_tx_thread_locker, gd.open_tx_thread, std::thread::id());
+                wait_pred(gd.heads_locker, [&]() { return gd.latest_complete_tx.load() == gd.manager_tx_head; });
                 update_when_ready(gd.open_tx_thread_locker,
                                 gd.open_tx_thread,
                                 std::thread::id(),
                                 this_id);
-            } else {
-                // We are a client, we need to wait for the manager to have caught
-                // up and no-one else is writing.
-
-                // Note: the two things we check are actually accessed using
-                // different locks. This is a little weird... and might be
-                // problematic?
-                while(gd.latest_complete_tx != gd.manager_tx_head.load()
-                    || gd.open_tx_thread != this_id) {
-                    // Give up the open_tx_thread if we stole it before the manager can catch up.
-                    if(gd.open_tx_thread == this_id)
-                        update(gd.open_tx_thread_locker, gd.open_tx_thread, std::thread::id());
-                    wait_pred(gd.heads_locker, [&]() { return gd.latest_complete_tx.load() == gd.manager_tx_head; });
-                    update_when_ready(gd.open_tx_thread_locker,
-                                    gd.open_tx_thread,
-                                    std::thread::id(),
-                                    this_id);
-                }
             }
         }
                           
@@ -946,53 +939,89 @@ namespace zefDB {
     void FinishTransaction(GraphData& gd) {
         FinishTransaction(gd, zwitch.default_wait_for_tx_finish());
     }
-
     void FinishTransaction(GraphData& gd, bool wait) {
+        FinishTransaction(gd, wait, zwitch.default_rollback_empty_tx());
+    }
+
+    void FinishTransaction(GraphData& gd, bool wait, bool rollback_empty_tx) {
+        FinishTransaction(gd, wait, rollback_empty_tx, true);
+    }
+
+    void FinishTransaction(GraphData& gd, bool wait, bool rollback_empty_tx, bool check_schema) {
         gd.number_of_open_tx_sessions--;
         // in case this was the last transaction that is closed, we want to mark the 
         // transcation node as complete: any write mod to the graph will trigger a new tx hereafter
         if (gd.number_of_open_tx_sessions == 0) {
-            if(gd.index_of_open_tx_node == 0) {
-                // If we get here, then the tx has been aborted, but we still need to unlock the GraphData for new transactions to start.
-                update(gd.open_tx_thread_locker, gd.open_tx_thread, std::thread::id());
-                return;
-            }
-            bool can_run_subs = gd.latest_complete_tx == gd.last_run_subscriptions.load();
-            gd.latest_complete_tx = gd.index_of_open_tx_node;
-            gd.index_of_open_tx_node = 0;
-			
-            // Unlike the write_head, we need to inform any listeners if the read_head changes.
-            update(gd.heads_locker, gd.read_head, gd.write_head.load());  // the zefscription manager can send out updates up to this pointer (not including)		
-            // TODO: This might not be the right place.
-            auto & info = MMap::info_from_blobs(&gd);
-            MMap::flush_mmap(info, gd.write_head);
+            blob_index manager_tx = 0;
+            {
+                RAII_CallAtEnd call_at_end([&]() {
+                    update(gd.open_tx_thread_locker, gd.open_tx_thread, std::thread::id());
+                });
+                
+                if(check_schema && gd.index_of_open_tx_node != 0) {
+                    EZefRef ezr_tx{gd.index_of_open_tx_node, gd};
+                    ZefRef ctx{ezr_tx, ezr_tx};
+                    // We fake that we have the transaction still open just for AbortTransaction
+                    gd.number_of_open_tx_sessions++;
+                    try {
+                        Butler::pass_to_schema_validator(ctx);
+                    } catch(const std::exception & e) {
+                        std::cerr << "Exception in schema_validator: " << e.what() << std::endl;
+                        AbortTransaction(Graph(ctx));
+                        gd.number_of_open_tx_sessions--;
+                        throw std::runtime_error(std::string("Schema validation failed: ") + e.what());
+                    }
+                    gd.number_of_open_tx_sessions--;
+                }
 
+                if(rollback_empty_tx && gd.index_of_open_tx_node != 0) {
+                    // The transaction is empty if the tx node is the last thing before the write head.
+                    blob_index next_node = gd.index_of_open_tx_node + blob_index_size(EZefRef{gd.index_of_open_tx_node, gd});
+                    if(next_node == gd.write_head.load()) {
+                        // We fake that we have the transaction still open just for AbortTransaction
+                        gd.number_of_open_tx_sessions++;
+                        AbortTransaction(gd);
+                        gd.number_of_open_tx_sessions--;
+                    }
+                }
+
+                // If we have been aborted, then don't continue for the rest of the logic
+                if(gd.index_of_open_tx_node == 0)
+                    return;
 
-            // Note: we have to give up the lock on the thread, as we could
-            // block waiting for the msg_queue of the graph manager in the next
-            // lines.
-            update(gd.open_tx_thread_locker, gd.open_tx_thread, std::thread::id());
+
+                // TODO: This might not be the right place.
+                auto & info = MMap::info_from_blobs(&gd);
+                MMap::flush_mmap(info, gd.write_head);
+
+                // Unlike the write_head, we need to inform any listeners if the read_head changes.
+                // update(gd.heads_locker, gd.read_head, gd.write_head.load());  // the zefscription manager can send out updates up to this pointer (not including)		
+                update(gd.heads_locker, [&]() {
+                    gd.read_head = gd.write_head.load();
+                    gd.latest_complete_tx = gd.index_of_open_tx_node;
+                    gd.index_of_open_tx_node = 0;
+                    manager_tx = gd.manager_tx_head;
+                });
+            }
+            // Let's check in this thread - here at least we should be able to see the next tx edge
+            EZefRef debug_tx{manager_tx, gd};
+            if(!(debug_tx | has_out[BT.NEXT_TX_EDGE])) {
+                std::cerr << "guid: " << uid(gd) << std::endl;
+                std::cerr << "CAN'T SEE NEXT_TX_EDGE EVEN FROM WITHIN FINISH TRANSACTION!!!!" << std::endl;
+            }
+
+            // Note: we have to give up the lock on the thread by this point, as
+            // we could block waiting for the msg_queue of the graph manager in
+            // the next lines.
 
             auto butler = Butler::get_butler();
             // False is because we don't want to wait for response
             butler->msg_push(Messages::NewTransactionCreated{Graph(gd), gd.latest_complete_tx}, false);
 
-            if(zwitch.write_thread_runs_subscriptions()) {
-                if (!can_run_subs) 
-                    return;
-                while(gd.last_run_subscriptions.load() < gd.latest_complete_tx) {
-                    EZefRef last_tx{gd.last_run_subscriptions, gd};
-                    EZefRef this_tx = last_tx >> BT.NEXT_TX_EDGE;
-                    run_subscriptions(gd, this_tx);
-                    update(gd.heads_locker, gd.last_run_subscriptions, index(this_tx));
-                }
-                internals::execute_queued_fcts(gd);
-            } else {
-                // Wait if requested and we aren't running subscriptions.
-                if(std::this_thread::get_id() != gd.sync_thread_id) {
-                    if(wait) {
-                        wait_pred(gd.heads_locker, [&]() { return gd.latest_complete_tx.load() == gd.manager_tx_head; });
-                    }
+            // Wait if requested and we aren't running subscriptions.
+            if(std::this_thread::get_id() != gd.sync_thread_id) {
+                if(wait) {
+                    wait_pred(gd.heads_locker, [&]() { return gd.latest_complete_tx.load() == gd.manager_tx_head; });
                 }
             }
         }
@@ -1015,6 +1044,8 @@ namespace zefDB {
 
 
     Transaction::Transaction(GraphData & gd) : Transaction(gd, zwitch.default_wait_for_tx_finish()) {}
+    Transaction::Transaction(GraphData & gd, bool wait) : Transaction(gd, wait, zwitch.default_rollback_empty_tx()) {}
+    Transaction::Transaction(GraphData & gd, bool wait, bool rollback_empty) : Transaction(gd, wait, rollback_empty, true) {}
 
     void run_subscriptions(GraphData & gd, EZefRef transaction_uzr) {
         if(!gd.observables)
diff --git a/core/src/high_level_api.cpp b/core/src/high_level_api.cpp
index a0e34024..15dbdd25 100644
--- a/core/src/high_level_api.cpp
+++ b/core/src/high_level_api.cpp
@@ -389,7 +389,7 @@ namespace zefDB {
         // * Connections
 
         bool HasIn::operator() (const EZefRef uzr) const {
-            return std::visit(overload{
+            return std::visit(overloaded{
                     [](Sentinel x)->bool {throw std::runtime_error("has_in used without curried in relation"); return false; },
                     [&uzr](auto relation)->bool { 
                         return length(uzr < L[relation]) >= 1;
@@ -397,7 +397,7 @@ namespace zefDB {
                 }, relation);
         }
         bool HasIn::operator() (const ZefRef zr) const {
-            return std::visit(overload{
+            return std::visit(overloaded{
                     [](Sentinel x)->bool {throw std::runtime_error("has_in used without curried in relation"); return false; },
                     [&zr](auto relation)->bool { 
                         return length(zr < L[relation]) >= 1;
@@ -406,7 +406,7 @@ namespace zefDB {
         }
 
         bool HasOut::operator() (const EZefRef uzr) const {
-            return std::visit(overload{
+            return std::visit(overloaded{
                     [](Sentinel x)->bool {throw std::runtime_error("has_out used without curried in relation"); return false; },
                     [&uzr](auto relation)->bool { 
                         return length(uzr > L[relation]) >= 1;
@@ -414,7 +414,7 @@ namespace zefDB {
                 }, relation);
         }
         bool HasOut::operator() (const ZefRef zr) const {
-            return std::visit(overload{
+            return std::visit(overloaded{
                     [](Sentinel x)->bool {throw std::runtime_error("has_out used without curried in relation"); return false; },
                     [&zr](auto relation)->bool { 
                         return length(zr > L[relation]) >= 1;
@@ -791,14 +791,14 @@ namespace zefDB {
 			}
 
 			ZefRefs Without::operator() (const ZefRefs& original) const {
-                return std::visit(overload{
+                return std::visit(overloaded{
                         [](Sentinel x)->ZefRefs {throw std::runtime_error("without used without curried in ZefRefs"); },
                         [&original,this](ZefRefs zrs)->ZefRefs {return Without::operator()(original, zrs);},
                         [](EZefRefs x)->ZefRefs {throw std::runtime_error("without used between ZefRefs and EZefRefs"); }
                     }, removing);
 			}
 			EZefRefs Without::operator() (const EZefRefs& original) const {
-                return std::visit(overload{
+                return std::visit(overloaded{
                         [](Sentinel x)->EZefRefs {throw std::runtime_error("without used without curried in EZefRefs"); },
                         [&original,this](EZefRefs uzrs)->EZefRefs {return Without::operator()(original, uzrs);},
                         [](ZefRefs x)->EZefRefs {throw std::runtime_error("without used between EZefRefs and ZefRefs"); }
@@ -1413,7 +1413,7 @@ namespace zefDB {
         EZefRef latest_tx = operator()(g) | to_ezefref;
                 if (!is_promotable_to_zefref(uzr, latest_tx)) throw std::runtime_error("Zefop 'now' called on EZefRef that cannot be promoted to a ZefRef.");
 
-				if (!zefOps::exists_at[latest_tx](uzr))
+				if (!imperative::exists_at_now(uzr))
 					throw std::runtime_error("'now(EZefRef)' called on a EZefRef that does not exist at the latest time slice. You can opt in to allow representing terminated RAEs by providing the flag 'z | now[allow_tombstone]' ");
 
 				return ZefRef(uzr, latest_tx);
@@ -1513,7 +1513,7 @@ namespace zefDB {
         } else {
             if (!internals::has_delegate(BT(uzr))) throw std::runtime_error("Instances(zr) called for a blob type where no delegate exists.");
 
-            EZefRef ref_frame_tx = std::visit(overload{
+            EZefRef ref_frame_tx = std::visit(overloaded{
                     [](Sentinel x)->EZefRef {throw std::runtime_error("a reference frame has to be specified and curried into the 'instance' zefop when piping through a delegate EZefRef."); },
                     [](EZefRef tx)->EZefRef { return tx; },
                     [](ZefRef tx)->EZefRef { return tx | to_ezefref; },
@@ -1526,7 +1526,7 @@ namespace zefDB {
     ZefRefs Instances::operator() (const Graph& g) const {
         if (std::holds_alternative<Sentinel>(ref_frame_data)) throw std::runtime_error("g | Instances was called, but no reference frame was set in the latter op.");
 
-        EZefRef ref_frame_tx = std::visit(overload{
+        EZefRef ref_frame_tx = std::visit(overloaded{
                 [](Sentinel x)->EZefRef {throw std::runtime_error("g | Instances was called, but no reference frame was set in the latter op."); },
                 [](EZefRef tx)->EZefRef { return tx; },
                 [](ZefRef tx)->EZefRef { return tx | to_ezefref; },
@@ -1721,7 +1721,7 @@ namespace zefDB {
 				bool is_out_relation = (std::holds_alternative<OnInstantiation>(relation_direction) && std::get<OnInstantiation>(relation_direction).is_outgoing) ||
 					(std::holds_alternative<OnTermination>(relation_direction) && std::get<OnTermination>(relation_direction).is_outgoing);
 				bool is_instantiation = std::holds_alternative<OnInstantiation>(relation_direction);
-				RelationType relation_type = std::visit(overload{
+				RelationType relation_type = std::visit(overloaded{
 					[](OnInstantiation op)->RelationType {if (!bool(op.rt)) throw std::runtime_error("rel not set in OnInstantiation used in 'subscribe'"); return *op.rt; },
 					[](OnTermination op)->RelationType {if (!bool(op.rt)) throw std::runtime_error("rel not set in OnTermination used in 'subscribe'"); return *op.rt; },
 
@@ -1763,7 +1763,7 @@ namespace zefDB {
 						}
 					};
 
-					ZefRef monitored_rel_list_entity = std::visit(overload{
+					ZefRef monitored_rel_list_entity = std::visit(overloaded{
 						[&g_observables, &get_or_create_monitored_rel_list_entity](OnInstantiation op)->ZefRef { return get_or_create_monitored_rel_list_entity(*op.rt, op.is_outgoing); },
 						[&g_observables, &get_or_create_monitored_rel_list_entity](OnTermination op)->ZefRef { return get_or_create_monitored_rel_list_entity(*op.rt, op.is_outgoing); },
 						[&g_observables](auto x)->ZefRef { return g_observables[42] | now; } // never reached
@@ -2248,25 +2248,7 @@ namespace zefDB {
 
 
 		bool is_terminated(EZefRef my_rel_ent) {
- 			assert_is_this_a_rae(my_rel_ent);
-			EZefRef this_re_ent_inst = get_RAE_INSTANCE_EDGE(my_rel_ent);
-			blob_index last_index = internals::last_set_edge_index(this_re_ent_inst);
-			EZefRef last_in_edge_on_scenario_node(last_index, *graph_data(my_rel_ent));
-
-			if (get<BlobType>(last_in_edge_on_scenario_node) == BlobType::TERMINATION_EDGE) return true;
-            // TODO: Turn this assert back on later.
-            // TODO: Turn this assert back on later.
-            // TODO: Turn this assert back on later.
-            // TODO: Turn this assert back on later.
-            // TODO: Turn this assert back on later.
-            // TODO: Turn this assert back on later.
-            // TODO: Turn this assert back on later.
-            // TODO: Turn this assert back on later.
-			// assert( // check that the other incoming edges are of reasonable type (i.e. expected to be incoming on scenario edge)
-			// 	get<BlobType>(last_in_edge_on_scenario_node) == BlobType::INSTANTIATION_EDGE ||
-			// 	get<BlobType>(last_in_edge_on_scenario_node) == BlobType::ATOMIC_VALUE_ASSIGNMENT_EDGE
-			// );
-			return false;
+            return !imperative::exists_at_now(my_rel_ent);
 		}
 
 
@@ -2462,7 +2444,7 @@ namespace zefDB {
 			}
 			// the foreign (atomic)entity is not in this graph
 			GraphData& gd = target_graph.my_graph_data();
-			EZefRef new_foreign_entity_uzr = std::visit(overload{
+			EZefRef new_foreign_entity_uzr = std::visit(overloaded{
 				[&](EntityType et) {
 					EZefRef new_foreign_entity_uzr = internals::instantiate(BT.FOREIGN_ENTITY_NODE, gd);
 					reinterpret_cast<blobs_ns::FOREIGN_ENTITY_NODE*>(new_foreign_entity_uzr.blob_ptr)->entity_type = et;
@@ -2621,22 +2603,21 @@ namespace zefDB {
             auto & gd = g.my_graph_data();
             LockGraphData lock{&gd};
 
-            TimeSlice latest = time_slice(now(g));
-            if(!exists_at(uzr, latest))
+            if(!exists_at_now(uzr))
                 throw std::runtime_error("Delegate is already retired");
             
             for(auto & parent : uzr >> L[BT.TO_DELEGATE_EDGE]) {
-                if(exists_at(parent, latest))
+                if(exists_at_now(parent))
                     throw std::runtime_error("Can't retire a delegate when it has a parent higher-order delegate.");
             }
 
             for(auto & rel : uzr | ins_and_outs) {
-                if(BT(rel) == BT.RELATION_EDGE && exists_at(rel, latest))
+                if(BT(rel) == BT.RELATION_EDGE && exists_at_now(rel))
                     throw std::runtime_error("Can't retire a delegate when it is the source/target of another instance/delegate.");
             }
 
             for(auto & instance : (uzr < BT.TO_DELEGATE_EDGE) >> L[BT.RAE_INSTANCE_EDGE]) {
-                if(exists_at(instance, latest))
+                if(exists_at_now(instance))
                     throw std::runtime_error("Can't retire a delegate when it has existing instances.");
             }
 
@@ -2725,6 +2706,46 @@ namespace zefDB {
             return exists_at(zr.blob_uzr, tx);
         }
 
+        //////////////////////////////
+        // * exists_at_now
+
+        // This is an optimised version that uses only the latest edge in the
+        // RAE_INSTANCE_EDGE or TO_DELEGATE_EDGE in order to determine whether
+        // the RAE/delegate is alive.
+        //
+        // This relies on the instantiations/terminations being sorted chronologically.
+		bool exists_at_now(EZefRef uzr) {
+            if(is_delegate(uzr)) {
+                EZefRef to_del = internals::get_TO_DELEGATE_EDGE(uzr);
+                EZefRef last_edge(internals::last_set_edge_index(to_del), *graph_data(uzr));
+
+                if (get<BlobType>(last_edge) == BlobType::DELEGATE_RETIREMENT_EDGE) return false;
+                assert(get<BlobType>(last_edge) == BlobType::DELEGATE_INSTANTIATION_EDGE
+                       || get<BlobType>(last_edge) == BlobType::RAE_INSTANCE_EDGE);
+                return true;
+            }
+
+            if(get<BlobType>(uzr) == BlobType::ROOT_NODE)
+                return true;
+
+            if(get<BlobType>(uzr) == BlobType::TX_EVENT_NODE)
+                return true;
+
+            // For RAEs
+            internals::assert_is_this_a_rae(uzr);
+			EZefRef this_re_ent_inst = internals::get_RAE_INSTANCE_EDGE(uzr);
+			blob_index last_index = internals::last_set_edge_index(this_re_ent_inst);
+			EZefRef last_in_edge_on_scenario_node(last_index, *graph_data(uzr));
+
+			if (get<BlobType>(last_in_edge_on_scenario_node) == BlobType::TERMINATION_EDGE) return false;
+            assert(get<BlobType>(last_in_edge_on_scenario_node) == BlobType::INSTANTIATION_EDGE
+                   || get<BlobType>(last_in_edge_on_scenario_node) == BlobType::ATOMIC_VALUE_ASSIGNMENT_EDGE
+                   || get<BlobType>(last_in_edge_on_scenario_node) == BlobType::ORIGIN_RAE_EDGE
+                   || get<BlobType>(last_in_edge_on_scenario_node) == BlobType::ASSIGN_TAG_NAME_EDGE);
+            return true;
+        }
+
+
         //////////////////////////////
         // * to_frame
 
@@ -3563,17 +3584,17 @@ namespace zefDB {
             return temp | only;
         }
 
-        std::optional<EZefRef> delegate_to_ezr(const DelegateEntity & d, int order, Graph g, bool create) {
+        std::optional<EZefRef> delegate_to_ezr(const EntityType & et, int order, Graph g, bool create) {
             auto & gd = g.my_graph_data();
             EZefRef root{constants::ROOT_NODE_blob_index, gd};
             EZefRef z = root;
             for(int i = 0 ; i < order ; i++) {
-                EZefRefs opts = filter(traverse_out_node_multi(z, BT.TO_DELEGATE_EDGE), d.et);
+                EZefRefs opts = filter(traverse_out_node_multi(z, BT.TO_DELEGATE_EDGE), et);
                 if(length(opts) == 0) {
                     if(create) {
                         EZefRef tx = internals::get_or_create_and_get_tx(gd);
                         EZefRef new_z = internals::instantiate(BT.ENTITY_NODE, gd);
-                        get<blobs_ns::ENTITY_NODE>(new_z).entity_type = d.et;
+                        get<blobs_ns::ENTITY_NODE>(new_z).entity_type = et;
                         get<blobs_ns::ENTITY_NODE>(new_z).instantiation_time_slice = get<blobs_ns::TX_EVENT_NODE>(tx).time_slice;
                         EZefRef new_to_delegate_edge = internals::instantiate(z, BT.TO_DELEGATE_EDGE, new_z, gd);
                         internals::instantiate(tx, BT.DELEGATE_INSTANTIATION_EDGE, new_to_delegate_edge, gd);
@@ -3588,17 +3609,17 @@ namespace zefDB {
             return z;
         }
 
-        std::optional<EZefRef> delegate_to_ezr(const DelegateAtomicEntity & d, int order, Graph g, bool create) {
+        std::optional<EZefRef> delegate_to_ezr(const AtomicEntityType & aet, int order, Graph g, bool create) {
             auto & gd = g.my_graph_data();
             EZefRef root{constants::ROOT_NODE_blob_index, gd};
             EZefRef z = root;
             for(int i = 0 ; i < order ; i++) {
-                EZefRefs opts = filter(traverse_out_node_multi(z, BT.TO_DELEGATE_EDGE), d.aet);
+                EZefRefs opts = filter(traverse_out_node_multi(z, BT.TO_DELEGATE_EDGE), aet);
                 if(length(opts) == 0) {
                     if(create) {
                         EZefRef tx = internals::get_or_create_and_get_tx(gd);
                         EZefRef new_z = internals::instantiate(BT.ATOMIC_ENTITY_NODE, gd);
-                        get<blobs_ns::ATOMIC_ENTITY_NODE>(new_z).my_atomic_entity_type = d.aet;
+                        get<blobs_ns::ATOMIC_ENTITY_NODE>(new_z).my_atomic_entity_type = aet;
                         get<blobs_ns::ATOMIC_ENTITY_NODE>(new_z).instantiation_time_slice = get<blobs_ns::TX_EVENT_NODE>(tx).time_slice;
                         EZefRef new_to_delegate_edge = internals::instantiate(z, BT.TO_DELEGATE_EDGE, new_z, gd);
                         internals::instantiate(tx, BT.DELEGATE_INSTANTIATION_EDGE, new_to_delegate_edge, gd);
@@ -3617,18 +3638,18 @@ namespace zefDB {
             return (z <= RT) && (source(z) == z) && (target(z) == z);
         }
 
-        std::optional<EZefRef> delegate_to_ezr(const DelegateRelationGroup & d, int order, Graph g, bool create) {
+        std::optional<EZefRef> delegate_to_ezr(const RelationType & rt, int order, Graph g, bool create) {
             auto & gd = g.my_graph_data();
             EZefRef root{constants::ROOT_NODE_blob_index, gd};
             EZefRef z = root;
             for(int i = 0 ; i < order ; i++) {
-                EZefRefs opts = filter(traverse_out_node_multi(z, BT.TO_DELEGATE_EDGE), d.rt);
+                EZefRefs opts = filter(traverse_out_node_multi(z, BT.TO_DELEGATE_EDGE), rt);
                 opts = filter(opts, is_delegate_relation_group);
                 if(length(opts) == 0) {
                     if(create) {
                         EZefRef tx = internals::get_or_create_and_get_tx(gd);
                         EZefRef new_z = internals::instantiate(BT.RELATION_EDGE, gd);
-                        get<blobs_ns::RELATION_EDGE>(new_z).relation_type = d.rt;
+                        get<blobs_ns::RELATION_EDGE>(new_z).relation_type = rt;
                         get<blobs_ns::RELATION_EDGE>(new_z).instantiation_time_slice = get<blobs_ns::TX_EVENT_NODE>(tx).time_slice;
                         get<blobs_ns::RELATION_EDGE>(new_z).source_node_index = index(new_z);
                         get<blobs_ns::RELATION_EDGE>(new_z).target_node_index = index(new_z);
@@ -3654,7 +3675,7 @@ namespace zefDB {
             std::optional<EZefRef> d_src = delegate_to_ezr(*(d.source), g, create, 1);
             std::optional<EZefRef> d_trg = delegate_to_ezr(*(d.target), g, create, 1);
 
-            auto rel_group = delegate_to_ezr(DelegateRelationGroup{d.rt}, 1, g, create);
+            auto rel_group = delegate_to_ezr(d.rt, 1, g, create);
             if(!rel_group)
                 return {};
             EZefRef z = *rel_group;
@@ -3701,6 +3722,7 @@ namespace zefDB {
                         EZefRef new_z = internals::instantiate(BT.TX_EVENT_NODE, gd);
                         EZefRef new_to_delegate_edge = internals::instantiate(z, BT.TO_DELEGATE_EDGE, new_z, gd);
                         internals::instantiate(tx, BT.DELEGATE_INSTANTIATION_EDGE, new_to_delegate_edge, gd);
+                        z = new_z;
                     } else
                         return {};
                 } else {
@@ -3725,6 +3747,7 @@ namespace zefDB {
                         EZefRef new_z = internals::instantiate(BT.ROOT_NODE, gd);
                         EZefRef new_to_delegate_edge = internals::instantiate(z, BT.TO_DELEGATE_EDGE, new_z, gd);
                         internals::instantiate(tx, BT.DELEGATE_INSTANTIATION_EDGE, new_to_delegate_edge, gd);
+                        z = new_z;
                     } else
                         return {};
                 } else {
@@ -3755,13 +3778,12 @@ namespace zefDB {
                 } else 
                     return {};
             } else {
-                auto ts = time_slice(now(g));
-                if(!exists_at(*res, ts)) {
+                if(!exists_at_now(*res)) {
                     if(create) {
                         // Assign new instantiation edges all the way down.
                         EZefRef tx = internals::get_or_create_and_get_tx(gd);
                         EZefRef cur_d = *res;
-                        while(!exists_at(cur_d, ts)) {
+                        while(!exists_at_now(cur_d)) {
                             internals::instantiate(tx, BT.DELEGATE_INSTANTIATION_EDGE, cur_d < BT.TO_DELEGATE_EDGE, gd);
                             cur_d = cur_d << BT.TO_DELEGATE_EDGE;
                         }
@@ -3788,11 +3810,11 @@ namespace zefDB {
                 // Note: we may end up with order 0 at the end here - this is
                 // acceptable, and means this is an instance!
                 if(ezr <= ET)
-                    return Delegate{order, DelegateEntity{ET(ezr)}};
+                    return Delegate{order, ET(ezr)};
                 if(ezr <= AET)
-                    return Delegate{order, DelegateAtomicEntity{AET(ezr)}};
+                    return Delegate{order, AET(ezr)};
                 if(ezr <= RT)
-                    return Delegate{order, DelegateRelationGroup{RT(ezr)}};
+                    return Delegate{order, RT(ezr)};
                 if(BT(ezr) == BT.TX_EVENT_NODE)
                     return Delegate{order, DelegateTX{}};
                 if(BT(ezr) == BT.ROOT_NODE)
@@ -4031,7 +4053,7 @@ namespace zefDB {
 
 		inline std::ostream& operator<< (std::ostream& o, AtomicTypeVariant atv) {
 			o << "<";
-			std::visit(overload{
+			std::visit(overloaded{
 				[&o](Sentinel x)->void { o << "not set"; },
 				[&o](TimeSlice x)->void { o << "TimeSlice specified: " << x.value; },
 				[&o](EZefRef x)->void { o << "EZefRef specified as ref tx: " << x; },
@@ -4260,7 +4282,7 @@ namespace zefDB {
 			GraphData& gd = *graph_data(my_atomic_entity);			
 			EZefRef ref_tx = std::holds_alternative<internals::Sentinel>(op.time_slice_override) ?
 				my_atomic_entity.tx :   // use the reference frame baked into the ZefRef if none is specified in the 'value' zefop
-				std::visit(overload{
+				std::visit(overloaded{
 					[&gd](internals::Sentinel ts)->EZefRef {return EZefRef(0, gd); },   // never used, but required for completion
 					[&gd](TimeSlice ts)->EZefRef {return tx[gd][ts]; },  // determine the tx event for the specified time slice here: before this it is not know for which graph the tx should be determined
 					[](EZefRef uzr)->EZefRef { return uzr; },   // if the tx is directly specified
@@ -4303,7 +4325,7 @@ namespace zefDB {
 				throw std::runtime_error("EZefRef | value.something called, but the specified return type does not agree with the type of the ATOMIC_ENTITY_NODE pointed to.");
 
 			GraphData& gd = *graph_data(my_atomic_entity);			
-			EZefRef ref_tx = std::visit(overload{
+			EZefRef ref_tx = std::visit(overloaded{
 					[&gd](internals::Sentinel ts)->EZefRef {throw std::runtime_error("Time slice info need to be set inside 'value' zefop when calling on a EZefRef"); return EZefRef(0, gd); },   // never used, but required for completion
 					[&gd](TimeSlice ts)->EZefRef {return tx[gd][ts]; },  // determine the tx event for the specified time slice here: before this it is not know for which graph the tx should be determined
 					[](EZefRef uzr)->EZefRef { return uzr; },   // if the tx is directly specified
diff --git a/core/src/mmap.cpp b/core/src/mmap.cpp
index 1ac2a944..b2cfb988 100644
--- a/core/src/mmap.cpp
+++ b/core/src/mmap.cpp
@@ -12,7 +12,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-#ifdef _MSC_VER
+#include "export_statement.h"
+
+#ifdef ZEF_WIN32
 #include "mmap_windows.cpp"
 #else
 
diff --git a/core/src/mmap_windows.cpp b/core/src/mmap_windows.cpp
index 84d6f4a8..8e2ff9e3 100644
--- a/core/src/mmap_windows.cpp
+++ b/core/src/mmap_windows.cpp
@@ -538,7 +538,7 @@ namespace zefDB {
             if(errno != ENOMEM)
                 throw std::runtime_error("Some other kind of error other than can't enlarge mapping.");
             return false;
-#elif defined(_MSC_VER)
+#elif defined(ZEF_WIN32)
             // TODO: this might be possible with AWE but will have to investigate that more thoroughly before launching into an implementation.
             return false;
 #else
@@ -571,7 +571,7 @@ namespace zefDB {
             if(new_ptr == MAP_FAILED)
                 throw std::runtime_error("Unable to remap and enlarge memory.");
             return new_ptr;
-#elif defined(_MSC_VER)
+#elif defined(ZEF_WIN32)
             void * new_ptr = WindowsMMap(fd, 0, new_size, nullptr);
             if (fd == 0) {
                 memcpy(new_ptr, old_ptr, old_size);
diff --git a/core/src/synchronization.cpp b/core/src/synchronization.cpp
index abf5e120..0ebb1274 100644
--- a/core/src/synchronization.cpp
+++ b/core/src/synchronization.cpp
@@ -183,7 +183,67 @@ namespace zefDB {
             }
         }
 
-        void verify_double_linking_partial() {
+        void undo_double_linking(GraphData& gd, blob_index start_index, blob_index end_index) {
+            // Cache whether the prior blob has been handled.
+            std::unordered_set<blob_index> handled_nodes;
+
+            blob_index cur_index = start_index;
+			while (cur_index < end_index) {
+                EZefRef ezr{cur_index,gd};
+
+                if(has_source_target_node(ezr)) {
+                    blob_index src = source_node_index(ezr);
+                    blob_index trg = target_node_index(ezr);
+
+                    for(blob_index orig_indx : {src, trg}) {
+                        if(orig_indx < start_index && handled_nodes.find(orig_indx) == handled_nodes.end()) {
+                            // Find the original blob and the latest deferred edge
+                            // list in the original section
+                            EZefRef orig_ezr{orig_indx, gd};
+
+                            EZefRef subseq_ezr = orig_ezr;
+
+                            blob_index * subseq_index;
+                            while(true) {
+                                subseq_index = subsequent_deferred_edge_list_index(subseq_ezr);
+                                if(*subseq_index == blobs_ns::sentinel_subsequent_index)
+                                    break;
+                                if(*subseq_index > start_index)
+                                    break;
+                                subseq_ezr = EZefRef{*subseq_index, gd};
+                            }
+
+                            // We remove all edges past the first one which is new.
+                            blob_index * list_start = edge_indexes(subseq_ezr);
+                            blob_index * new_point = list_start;
+                            while(abs(*new_point) < start_index)
+                                new_point++;
+
+                            int num = local_edge_indexes_capacity(subseq_ezr);
+                            // Does this need a sizeof?
+                            int new_ind = (new_point - list_start);
+                            memset(new_point, 0, (num - new_ind)*sizeof(blob_index));
+
+                            // Also remove any deferred edge list (no need to check, just always do it)
+                            *subseq_index = blobs_ns::sentinel_subsequent_index;
+
+                            // And finally update the last_blob
+                            blob_index * last_blob = internals::last_edge_holding_blob(orig_ezr);
+                            if(new_point == list_start)
+                                *last_blob = 0;
+                            else {
+                                uintptr_t direct_ptr = (uintptr_t)new_point;
+                                blob_index * ptr = (blob_index*)(direct_ptr - (direct_ptr % constants::blob_indx_step_in_bytes));
+                                *last_blob = blob_index_from_ptr(ptr);
+                            }
+
+                            handled_nodes.insert(src);
+                        }
+                    }
+                }
+
+                cur_index += blob_index_size(ezr);
+            }
         }
 
         void apply_action_blob(GraphData& gd, EZefRef uzr_to_blob, bool fill_caches) {
diff --git a/core/src/tokens.cpp b/core/src/tokens.cpp
index cc9c5d4a..0a44d8d3 100644
--- a/core/src/tokens.cpp
+++ b/core/src/tokens.cpp
@@ -798,44 +798,50 @@ namespace zefDB {
     }
 
     Delegate delegate_of(EntityType et) {
-        return Delegate{1, DelegateEntity{et}};
+        return Delegate{1, et};
     }
     Delegate delegate_of(AtomicEntityType aet) {
-        return Delegate{1, DelegateAtomicEntity{aet}};
+        return Delegate{1, aet};
     }
     Delegate delegate_of(RelationType rt) {
-        return Delegate{1, DelegateRelationGroup{rt}};
+        return Delegate{1, rt};
     }
     Delegate delegate_of(const Delegate & d) {
         return Delegate{d.order+1, d.item};
     }
 
-    std::ostream & operator<<(std::ostream & o, const DelegateEntity & d) {
-        return o << "d" << d.et;
-    }
-    std::ostream & operator<<(std::ostream & o, const DelegateAtomicEntity & d) {
-        return o << "d" << d.aet;
-    }
-    std::ostream & operator<<(std::ostream & o, const DelegateRelationGroup & d) {
-        return o << "d" << d.rt;
+    void show_wrapper(std::ostream & o, const Delegate & d, bool outer) {
+        if(outer || d.order > 0)
+            o << "D" << d.order;
+        std::visit(overloaded {
+                [&](const DelegateRelationTriple & x) {
+                    o << x;
+                },
+                [&](auto & x) {
+                    if(outer || d.order > 0)
+                        o << "(" << x << ")";
+                    else
+                        o << x;
+                }}, d.item);
     }
+
     std::ostream & operator<<(std::ostream & o, const DelegateTX & d) {
-        return o << "dTX";
+        return o << "TX";
     }
     std::ostream & operator<<(std::ostream & o, const DelegateRoot & d) {
-        return o << "dRoot";
+        return o << "Root";
     }
     std::ostream & operator<<(std::ostream & o, const DelegateRelationTriple & d) {
-        o << "{" << *d.source << ">" << "d" << d.rt << ">" << *d.target << "}";
+        o << "{";
+        show_wrapper(o, *d.source, false);
+        o << "," << d.rt << ",";
+        show_wrapper(o, *d.target, false);
+        o << "}";
         return o;
     }
 
     std::ostream & operator<<(std::ostream & o, const Delegate & d) {
-        o << "D" << d.order << "(";
-        std::visit([&](auto & x) {
-            o << x;
-        }, d.item);
-        o << ")";
+        show_wrapper(o, d, true);
         return o;
     }
 
diff --git a/core/src/verification.cpp b/core/src/verification.cpp
index 6ad7a704..dd5a420d 100644
--- a/core/src/verification.cpp
+++ b/core/src/verification.cpp
@@ -178,6 +178,57 @@ namespace zefDB {
 			return true;
 		}
 
+
+        bool verify_chronological_instantiation_order(Graph g) {
+			GraphData& gd = g.my_graph_data();
+
+            blob_index cur_index = internals::root_node_blob_index();
+			while (cur_index < gd.write_head) {
+                EZefRef uzr{cur_index,gd};
+
+                if(get<BlobType>(uzr) == BlobType::RAE_INSTANCE_EDGE) {
+                    TimeSlice last_ts(0);
+                    auto all_edges = ins_and_outs(uzr);
+                    for (auto edge_uzr : all_edges) {
+                        if(get<BlobType>(edge_uzr) == BlobType::INSTANTIATION_EDGE ||
+                           get<BlobType>(edge_uzr) == BlobType::TERMINATION_EDGE) {
+                            EZefRef tx_uzr = source(edge_uzr);
+                            auto & tx_node = get<blobs_ns::TX_EVENT_NODE>(tx_uzr);
+                            if(tx_node.time_slice < last_ts) {
+                                std::cerr << "Chronological order is bad for blob: " << cur_index << std::endl;
+                                return false;
+                            }
+
+                            last_ts = tx_node.time_slice;
+                        }
+                    }
+                } else if(get<BlobType>(uzr) == BlobType::TO_DELEGATE_EDGE) {
+                    TimeSlice last_ts(0);
+                    auto all_edges = ins_and_outs(uzr);
+                    for (auto edge_uzr : all_edges) {
+                        if(get<BlobType>(edge_uzr) == BlobType::DELEGATE_INSTANTIATION_EDGE ||
+                           get<BlobType>(edge_uzr) == BlobType::DELEGATE_RETIREMENT_EDGE) {
+                            EZefRef tx_uzr = source(edge_uzr);
+                            auto & tx_node = get<blobs_ns::TX_EVENT_NODE>(tx_uzr);
+                            if(tx_node.time_slice < last_ts) {
+                                std::cerr << "Chronological order is bad for blob: " << cur_index << std::endl;
+                                return false;
+                            }
+
+                            last_ts = tx_node.time_slice;
+                        }
+                    }
+                }
+
+                cur_index += blob_index_size(uzr);
+            }
+            
+            return true;
+        }
+
+
+        // This was for some kind of testing
+
         void break_graph(Graph&g, blob_index index, int style) {
             EZefRef ezr = g[index];
             if(style == 1) {
@@ -192,6 +243,28 @@ namespace zefDB {
                 visit_blob_with_edges([](auto & x) {
                     x.edges.indices[0] = 42;
                 }, ezr);
+            } else if(style == 4) {
+                // Find a RAE instance that has been terminated and flip it
+                if(get<BlobType>(ezr) != BlobType::RAE_INSTANCE_EDGE)
+                    throw std::runtime_error("Index needs to be for a RAE_INSTANCE_EDGE");
+
+                auto all_edges = ins_and_outs(ezr);
+                int num_inst_term = 0;
+                for(auto edge_ezr : all_edges) {
+                    if(get<BlobType>(edge_ezr) == BlobType::INSTANTIATION_EDGE ||
+                        get<BlobType>(edge_ezr) == BlobType::TERMINATION_EDGE) {
+                        num_inst_term++;
+                    }
+                }
+                if(num_inst_term < 2)
+                    throw std::runtime_error("RAE needs two inst/terms");
+
+                visit_blob_with_edges([](auto & x) {
+                    blob_index temp = x.edges.indices[1];
+                    x.edges.indices[1] = x.edges.indices[0];
+                    x.edges.indices[0] = temp;
+                }, ezr);
+
             } else {
                 throw std::runtime_error("Don't know style");
             }
diff --git a/core/src/zef_config.cpp b/core/src/zef_config.cpp
new file mode 100644
index 00000000..dd2026f8
--- /dev/null
+++ b/core/src/zef_config.cpp
@@ -0,0 +1,232 @@
+// Copyright 2022 Synchronous Technologies Pte Ltd
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+#include "zef_config.h"
+#include "butler/butler.h"
+
+#include "yaml-cpp/yaml.h"
+
+namespace zefDB {
+
+    const ConfigItem config_spec[] = {
+        {"login.autoConnect", "auto", {"no", "auto", "always"}, "ZEFDB_LOGIN_AUTOCONNECT",
+         "Whether zefdb should automatically connect to ZefHub on start of the butler."},
+        {"butler.autoStart", true, {}, "ZEFDB_BUTLER_AUTOSTART",
+         "Whether the butler will automatically be started on import of the zefdb module."},
+        {"login.zefhubURL", "wss://hub.zefhub.io", {}, "ZEFHUB_URL",
+         "Which URL to connect to ZefHub."},
+    };
+    const int num_config_spec = sizeof(config_spec) / sizeof(ConfigItem);
+
+    // Going to make this code trivially threadsafe by locking before each function call.
+    std::recursive_mutex config_mutex;
+
+    
+    std::unordered_map<std::string, config_var_t> session_overrides;
+
+    std::filesystem::path config_file_path() {
+        return Butler::zefdb_config_path() / "config.yaml";
+    }
+
+    bool config_file_exists() {
+        return std::filesystem::exists(config_file_path());
+    }
+
+    void ensure_config_file() {
+        if(config_file_exists())
+            return;
+
+        std::filesystem::create_directories(config_file_path().parent_path());
+        std::ofstream touch(config_file_path());
+    }
+
+    const ConfigItem & get_spec(std::string key) {
+        // for(auto & item : config_spec) {
+        //     if(item.path == key)
+        //         return item;
+        // }
+        for(int i = 0; i < num_config_spec ; i++) {
+            if(config_spec[i].path == key)
+                return config_spec[i];
+        }
+        throw std::runtime_error("Don't recognise path '" + key + "' in config");
+    }
+
+    std::vector<std::string> split_path(std::string key) {
+        std::vector<std::string> out;
+        int p = -1;
+        while(true) {
+            int last_p = p+1;
+            p = key.find('.', last_p);
+            if(p == std::string::npos) {
+                out.push_back(key.substr(last_p));
+                break;
+            }
+            out.push_back(key.substr(last_p, p));
+        }
+        return out;
+    }
+
+    config_var_t get_config_var(std::string key) {
+        std::lock_guard lock(config_mutex);
+        auto search = session_overrides.find(key);
+        if(search != session_overrides.cend()) {
+            return search->second;
+        }
+
+        auto & spec = get_spec(key);
+        char * env = getenv(spec.env_var.c_str());
+        if(env != NULL) {
+            return std::visit(overloaded {
+                    [&env](const bool&) -> config_var_t { return parse_string_bool(std::string(env)); },
+                    [&env](const int&) -> config_var_t { return std::stoi(std::string(env)); },
+                    [&env](const double&) -> config_var_t { return std::stod(std::string(env)); },
+                    [&env](const std::string&) -> config_var_t { return std::string(env); },
+                }, spec.default_value);
+        }
+
+        if(config_file_exists()) {
+            YAML::Node config = YAML::LoadFile(config_file_path().string());
+            // TODO: What happens with a bad config file?
+
+            for(auto section : split_path(key)) {
+                config = config[section];
+            }
+
+            if(config.IsDefined()) {
+                auto value = std::visit(overloaded {
+                        [&config](const bool&) -> config_var_t { return config.as<bool>(); },
+                        [&config](const int&) -> config_var_t { return config.as<int>(); },
+                        [&config](const double&) -> config_var_t { return config.as<double>(); },
+                        [&config](const std::string&) -> config_var_t { return config.as<std::string>(); },
+                    }, spec.default_value);
+
+                if(spec.options.size() == 0)
+                    return value;
+
+                for(auto & opt : spec.options) {
+                    if(opt == value)
+                        return value;
+                }
+
+                throw std::runtime_error("Config option '" + key + "' has a value which is not allowed by the options.");
+            }
+        }
+
+        return spec.default_value;
+    }
+
+    // x get_full_config()
+    // TODO: Has to lookup every key for defaults/overrides and indicate this too in the return
+    
+    void set_config_var(std::string key, config_var_t val) {
+        ensure_config_file();
+
+        auto & spec = get_spec(key);
+
+        if(spec.default_value.index() != val.index())
+            throw std::runtime_error("Trying to set config variable '" + key + "' to a type (" + to_str(val.index()) + ") which is not the same as the default (" + to_str(spec.default_value.index()) + ")");
+
+        if(spec.options.size() > 0) {
+            bool found = false;
+            for(auto & opt : spec.options) {
+                if(opt == val) {
+                    found = true;
+                    break;
+                }
+            }
+            if(!found)
+                throw std::runtime_error("Trying to set config variable '" + key + "' to a value which is not in the allowed options.");
+        }
+
+        try {
+            YAML::Node config = YAML::LoadFile(config_file_path().string());
+
+            auto sections = split_path(key);
+            auto last = sections.end() - 1;
+            YAML::Node config_part;
+            for(auto it = sections.begin() ; it != last ; it++) {
+                auto section = *it;
+                // This is the weird part
+                if(it == sections.begin()) {
+                    config_part = config[section];
+                } else {
+                    config_part = config_part[section];
+                }
+
+            }
+
+            std::visit([&config_part,&last](auto & _val) {
+                config_part[*last] = _val;
+            }, val);
+
+            std::ofstream fout(config_file_path());
+
+            fout << config;
+
+        } catch(...) {
+            std::cerr << "Some kind of error when saving config file. Still assign varaible for session." << std::endl;
+        }
+
+        session_overrides[key] = val;
+    }
+
+    std::vector<std::pair<std::string,config_var_t>> list_config(std::string filter) {
+        std::vector<std::pair<std::string,config_var_t>> out;
+
+        for(int i = 0; i < num_config_spec ; i++) {
+            if(filter != "" && config_spec[i].path.find(filter) == std::string::npos)
+                continue;
+
+            out.emplace_back(config_spec[i].path, get_config_var(config_spec[i].path));
+        }
+
+        return out;
+    }
+
+    void recurse_validate(std::string parent_path, YAML::Node self) {
+        if(self.IsMap()) {
+            for(auto item : self) {
+                std::string new_path = parent_path;
+                if(parent_path != "")
+                    new_path += ".";
+                new_path += item.first.as<std::string>();
+                recurse_validate(new_path, item.second);
+            }
+        } else {
+            // If the path is "" then this is likely that the config file is empty.
+            if(parent_path != "")
+                // We only care about the path, not the value in it.
+                get_spec(parent_path);
+        }
+    };
+
+    bool validate_config_file() {
+        try {
+            // Get all variables so that everything is checked.
+            list_config();
+
+            // Also check every entry in the file corresponds to something in the spec.
+            if(config_file_exists()) {
+                YAML::Node config = YAML::LoadFile(config_file_path().string());
+                recurse_validate("", config);
+            }
+        } catch(const std::exception & exc) {
+            std::cerr << "validate_config_file got an exception: " << exc.what() << std::endl;
+            return false;
+        }
+
+        return true;
+    }
+}
\ No newline at end of file
diff --git a/dockerfiles/Dockerfile.base b/dockerfiles/Dockerfile.base
new file mode 100644
index 00000000..a8c1677a
--- /dev/null
+++ b/dockerfiles/Dockerfile.base
@@ -0,0 +1,30 @@
+FROM python:3.9-slim-bullseye AS builder
+
+RUN apt-get update
+RUN apt-get install -y --no-install-recommends \
+    build-essential \
+    cmake \
+    make \
+    ninja-build \
+    curl \
+    git \
+    zstd \
+    libzstd-dev \
+    openssl \
+    libssl-dev \
+    libsecret-1-0 \
+    libcurl4-openssl-dev \
+    python3 \
+    python3-pip \
+    python3-dev \
+    jq
+
+COPY . .
+
+RUN LIBZEF_LOCATION=$(realpath core) pip3 install ./python
+
+# Final light image
+FROM python:3.9-slim-bullseye
+
+COPY --from=builder /usr/local/lib/python3.9/site-packages /usr/local/lib/python3.9/site-packages
+
diff --git a/python/pyzef/CMakeLists.txt b/python/pyzef/CMakeLists.txt
index 721e15e8..dcc97f71 100644
--- a/python/pyzef/CMakeLists.txt
+++ b/python/pyzef/CMakeLists.txt
@@ -100,6 +100,14 @@ set_target_properties(pyzef
     PROPERTIES
     CXX_STANDARD 17)
 
+if(CMAKE_BUILD_TYPE STREQUAL "Debug")
+  if(NOT WIN32)
+    target_compile_definitions(pyzef PRIVATE DEBUG)
+    target_compile_options(pyzef PRIVATE -rdynamic -ggdb -g)
+  endif()
+endif()
+
+
 # target_include_directories (pyzef PUBLIC ${RANGE_INCLUDE_DIRS})
 
 # if(UNIX AND NOT APPLE)
diff --git a/python/pyzef/build_zefDB.cpp b/python/pyzef/build_zefDB.cpp
index 89fac4cc..18c27e95 100644
--- a/python/pyzef/build_zefDB.cpp
+++ b/python/pyzef/build_zefDB.cpp
@@ -136,8 +136,8 @@ PYBIND11_MODULE(pyzef, toplevel_module) {
         .def("throw_on_zefrefs_no_tx", py::overload_cast<bool>(&Zwitch::throw_on_zefrefs_no_tx))
         .def("default_wait_for_tx_finish", py::overload_cast<>(&Zwitch::default_wait_for_tx_finish, py::const_))
         .def("default_wait_for_tx_finish", py::overload_cast<bool>(&Zwitch::default_wait_for_tx_finish))
-        .def("write_thread_runs_subscriptions", py::overload_cast<>(&Zwitch::write_thread_runs_subscriptions, py::const_))
-        .def("write_thread_runs_subscriptions", py::overload_cast<bool>(&Zwitch::write_thread_runs_subscriptions))
+        .def("default_rollback_empty_tx", py::overload_cast<>(&Zwitch::default_rollback_empty_tx, py::const_))
+        .def("default_rollback_empty_tx", py::overload_cast<bool>(&Zwitch::default_rollback_empty_tx))
 		.def("as_dict", &Zwitch::as_dict)
 		;
 	main_module.attr("zwitch") = &zwitch;  // expose this singleton
@@ -591,7 +591,9 @@ PYBIND11_MODULE(pyzef, toplevel_module) {
 	main_module.def("tag", py::overload_cast<EZefRef,const std::string,bool>(&tag), py::call_guard<py::gil_scoped_release>(), "Add a name tag / key_dict entry to for a specific ZefRef:  tag(my_z, 'my_favorite_zefref')", "z"_a, "name_tag"_a, "force_if_name_tags_other_rel_ent"_a=false);
 
 	verification_submodule.def("verify_graph_double_linking", verification::verify_graph_double_linking, "Check that node/edge linking is consistent for the indexes.");
+	verification_submodule.def("verify_chronological_instantiation_order", verification::verify_chronological_instantiation_order, "Check that RAEs and delegates have a correct chronological instantiation order.");
 	verification_submodule.def("break_graph", verification::break_graph, "Internal use checks");
+	verification_submodule.def("verify_graph", verification::verify_graph, "Internal use checks");
 
 	admins_submodule.def("add_user", [](std::string username, std::string key) { zefDB::user_management("add_user", username, "", key);});
 	admins_submodule.def("reset_user_key", [](std::string username, std::string key) { zefDB::user_management("reset_user_key", username, "", key);});
@@ -640,6 +642,12 @@ PYBIND11_MODULE(pyzef, toplevel_module) {
         ;
 
 
+    main_module.def("get_config_var", &get_config_var, py::call_guard<py::gil_scoped_release>(), "Get a variable with defaults/override resolution from the config.", py::arg("key"));
+    main_module.def("set_config_var", &set_config_var, py::call_guard<py::gil_scoped_release>(), "Set a variable in the config.", py::arg("key"), py::arg("value"));
+    main_module.def("list_config", &list_config, py::call_guard<py::gil_scoped_release>(), "List the config including all default/environment set variables.", py::arg("filter")="");
+    main_module.def("validate_config_file", &validate_config_file, py::call_guard<py::gil_scoped_release>(), "Ensure the config file and environment overrides have sensible values.");
+
+
 
 	fill_internals_module(internals_submodule);
 	create_zefops_module(toplevel_module, internals_submodule);
diff --git a/python/pyzef/internals.cpp b/python/pyzef/internals.cpp
index 1a61b390..61563b24 100644
--- a/python/pyzef/internals.cpp
+++ b/python/pyzef/internals.cpp
@@ -95,6 +95,11 @@ void fill_internals_module(py::module_ & internals_submodule) {
         auto butler = Butler::get_butler();
         butler->stop_connection();
     }, py::call_guard<py::gil_scoped_release>());
+
+    internals_submodule.def("who_am_i", []() {
+        auto butler = Butler::get_butler();
+        return butler->who_am_i();
+    }, py::call_guard<py::gil_scoped_release>());
         
 
 	internals_submodule.def("is_any_UID", &is_any_UID);
@@ -379,24 +384,6 @@ void fill_internals_module(py::module_ & internals_submodule) {
 	// internals_submodule.def("process_zm_tasks", [](){ tasks::apply_immediate_updates_from_zm(); }, py::call_guard<py::gil_scoped_release>());
 
 
-    py::class_<DelegateEntity>(internals_submodule, "DelegateEntity", py::buffer_protocol(), py::call_guard<py::gil_scoped_release>())
-        .def(py::init<EntityType>(), py::call_guard<py::gil_scoped_release>())
-		.def_readonly("et", &DelegateEntity::et)
-        .def("__repr__", [](const DelegateEntity & d) { return to_str(d); })
-		.def("__eq__", &DelegateEntity::operator==, py::is_operator())
-        ;
-    py::class_<DelegateAtomicEntity>(internals_submodule, "DelegateAtomicEntity", py::buffer_protocol(), py::call_guard<py::gil_scoped_release>())
-        .def(py::init<AtomicEntityType>(), py::call_guard<py::gil_scoped_release>())
-		.def_readonly("aet", &DelegateAtomicEntity::aet)
-        .def("__repr__", [](const DelegateAtomicEntity & d) { return to_str(d); })
-		.def("__eq__", &DelegateAtomicEntity::operator==, py::is_operator())
-        ;
-    py::class_<DelegateRelationGroup>(internals_submodule, "DelegateRelationGroup", py::buffer_protocol(), py::call_guard<py::gil_scoped_release>())
-        .def(py::init<RelationType>(), py::call_guard<py::gil_scoped_release>())
-		.def_readonly("rt", &DelegateRelationGroup::rt)
-        .def("__repr__", [](const DelegateRelationGroup & d) { return to_str(d); })
-		.def("__eq__", &DelegateRelationGroup::operator==, py::is_operator())
-        ;
     py::class_<DelegateTX>(internals_submodule, "DelegateTX", py::buffer_protocol(), py::call_guard<py::gil_scoped_release>())
         .def(py::init<>(), py::call_guard<py::gil_scoped_release>())
         .def("__repr__", [](const DelegateTX & d) { return to_str(d); })
@@ -417,9 +404,9 @@ void fill_internals_module(py::module_ & internals_submodule) {
         ;
 
     py::class_<Delegate>(internals_submodule, "Delegate", py::buffer_protocol(), py::call_guard<py::gil_scoped_release>(), py::dynamic_attr())
-        .def(py::init<int,DelegateEntity>(), py::call_guard<py::gil_scoped_release>())
-        .def(py::init<int,DelegateAtomicEntity>(), py::call_guard<py::gil_scoped_release>())
-        .def(py::init<int,DelegateRelationGroup>(), py::call_guard<py::gil_scoped_release>())
+        .def(py::init<int,EntityType>(), py::call_guard<py::gil_scoped_release>())
+        .def(py::init<int,AtomicEntityType>(), py::call_guard<py::gil_scoped_release>())
+        .def(py::init<int,RelationType>(), py::call_guard<py::gil_scoped_release>())
         .def(py::init<int,DelegateTX>(), py::call_guard<py::gil_scoped_release>())
         .def(py::init<int,DelegateRoot>(), py::call_guard<py::gil_scoped_release>())
         .def(py::init<int,DelegateRelationTriple>(), py::call_guard<py::gil_scoped_release>())
@@ -427,6 +414,7 @@ void fill_internals_module(py::module_ & internals_submodule) {
         .def(py::init<AtomicEntityType>(), py::call_guard<py::gil_scoped_release>())
         .def(py::init<RelationType>(), py::call_guard<py::gil_scoped_release>())
         .def(py::init<Delegate,RelationType,Delegate>(), py::call_guard<py::gil_scoped_release>())
+        .def(py::init<Delegate>(), py::call_guard<py::gil_scoped_release>())
 		.def_readonly("order", &Delegate::order)
 		.def_readonly("item", &Delegate::item)
         .def("__repr__", [](const Delegate & d) { return to_str(d); })
@@ -627,6 +615,8 @@ void fill_internals_module(py::module_ & internals_submodule) {
 		;
 
 	internals_submodule.def("StartTransactionReturnTx", [](Graph& g)->ZefRef { return StartTransactionReturnTx(g); }, py::call_guard<py::gil_scoped_release>());
+	internals_submodule.def("FinishTransaction", py::overload_cast<Graph&,bool,bool,bool>(&FinishTransaction), py::arg("g"), py::arg("wait"), py::arg("rollback_empty"), py::arg("check_schema"), py::call_guard<py::gil_scoped_release>());
+	internals_submodule.def("FinishTransaction", py::overload_cast<Graph&,bool,bool>(&FinishTransaction), py::arg("g"), py::arg("wait"), py::arg("rollback_empty"), py::call_guard<py::gil_scoped_release>());
 	internals_submodule.def("FinishTransaction", py::overload_cast<Graph&,bool>(&FinishTransaction), py::arg("g"), py::arg("wait"), py::call_guard<py::gil_scoped_release>());
 	internals_submodule.def("FinishTransaction", py::overload_cast<Graph&>(&FinishTransaction), py::arg("g"), py::call_guard<py::gil_scoped_release>());
 	internals_submodule.def("AbortTransaction", py::overload_cast<Graph&>(&AbortTransaction), py::call_guard<py::gil_scoped_release>());
@@ -690,7 +680,9 @@ void fill_internals_module(py::module_ & internals_submodule) {
 	internals_submodule.def("initialise_butler", py::overload_cast<>(&Butler::initialise_butler));
 	internals_submodule.def("initialise_butler", py::overload_cast<std::string>(&Butler::initialise_butler));
 	internals_submodule.def("initialise_butler_as_master", &Butler::initialise_butler_as_master);
-	internals_submodule.def("stop_butler", &Butler::stop_butler);
+    // Note: stopping the butler can cause python functions to be removed from
+    // subscriptions, so the GIL needs to be released.
+	internals_submodule.def("stop_butler", &Butler::stop_butler, py::call_guard<py::gil_scoped_release>());
 	internals_submodule.def("root_node_blob_index", internals::root_node_blob_index, "which blob index does the root node have?");
 	internals_submodule.def("validate_message_version", &Messages::validate_message_version);
 	internals_submodule.def("early_token_list", &Butler::early_token_list);
@@ -756,4 +748,7 @@ void fill_internals_module(py::module_ & internals_submodule) {
 
     internals_submodule.def("register_merge_handler", &Butler::register_merge_handler);
     internals_submodule.add_object("_cleanup_merge_handler", py::capsule(&Butler::remove_merge_handler));
+
+    internals_submodule.def("register_schema_validator", &Butler::register_schema_validator);
+    internals_submodule.add_object("_cleanup_schema_validator", py::capsule(&Butler::remove_schema_validator));
 }
diff --git a/python/zef/__init__.py b/python/zef/__init__.py
index a73e14c7..486498d4 100644
--- a/python/zef/__init__.py
+++ b/python/zef/__init__.py
@@ -41,7 +41,7 @@
 # Putting this into a function to avoid polluting namespace
 def _autostart_behaviour():
     auto_start = ops.config("butler.autoStart", KW.get)
-    if auto_start == "true":
+    if auto_start:
         core.internals.initialise_butler()
 
 _autostart_behaviour()
diff --git a/python/zef/core/VT/__init__.py b/python/zef/core/VT/__init__.py
index 72a41d76..bc3ed84c 100644
--- a/python/zef/core/VT/__init__.py
+++ b/python/zef/core/VT/__init__.py
@@ -41,11 +41,80 @@ def decimal_ctor(*args, **kwargs):
     # return core.bytes.Bytes_(*args, **kwargs)
     return Decimal_(*args, **kwargs)
 
-# def error_ctor(*args, **kwargs):
-#     from ... import core
-#     return core.error._Error.Error(*args, **kwargs)
 
 
+def union_getitem(x):
+    from ..op_structs import ZefOp
+    if isinstance(x, tuple):
+        return ValueType_(type_name='Union', absorbed=x)
+    elif isinstance(x, ValueType_):
+        return ValueType_(type_name='Union', absorbed=(x,))
+    elif isinstance(x, ZefOp):
+        return ValueType_(type_name='Union', absorbed=(x,))
+    else:
+        raise Exception(f'"Union[...]" called with unsupported type {type(x)}')
+
+def intersection_getitem(x):
+    from ..op_structs import ZefOp
+    if isinstance(x, tuple):
+        return ValueType_(type_name='Intersection', absorbed=x)
+    elif isinstance(x, ValueType_):
+        return ValueType_(type_name='Intersection', absorbed=(x,))
+    elif isinstance(x, ZefOp):
+        return ValueType_(type_name='Intersection', absorbed=(x,))
+    else:
+        raise Exception(f'"Intersection[...]" called with unsupported type {type(x)}')
+
+def complement_getitem(x):
+    if isinstance(x, ValueType_):
+        return ValueType_(type_name='Complement', absorbed=(x,))
+    else:
+        raise Exception(f'"Complement[...]" called with unsupported type {type(x)}')
+
+def is_getitem(x):
+    from typing import Callable
+    from ..op_structs import ZefOp
+    from .. import func
+    if isinstance(x, tuple):
+        return ValueType_(type_name='Is', absorbed=x)
+    elif isinstance(x, ValueType_):
+        return ValueType_(type_name='Is', absorbed=(x,))
+    elif isinstance(x, ZefOp):
+        return ValueType_(type_name='Is', absorbed=(x,))
+    elif isinstance(x, Callable):
+        return ValueType_(type_name='Is', absorbed=(func[x],))
+    else:
+        raise Exception(f'"Is[...]" called with unsupported type {type(x)}')
+
+
+
+def setof_ctor(*args):
+    """
+    Can be called with either SetOf[5,6,7] or SetOf(5,6,7).
+    When calling with square brackets, a tuple must always be 
+    passed implicitly or explicitly.
+    SetOf[42] is NOT valid
+    SetOf[(42,)] is valid
+    SetOf[42,] is valid    
+    SetOf[42, 43] is valid    
+    """
+    return ValueType_(type_name='SetOf', absorbed = (args, ))
+
+
+
+def setof_getitem(x):
+    if not isinstance(x, tuple):
+        raise TypeError(f"`SetOf[...]` must be called with a tuple, either explicitly or implicitly. e.g. SetOf[3,4], SetOf[(3,4)]. When wrapping one value, use SetOf[42,]. It was called with {x}. ")
+    return ValueType_(type_name='SetOf', absorbed=(x, ))
+
+
+
+
+def rp_getitem(x):
+    if not isinstance(x, tuple) or len(x)!=3:
+        raise TypeError(f"`RP`[...]  must be initialized with a triple to match on. Got x={x}")
+    return ValueType_(type_name='RP', absorbed=(x,))
+      
 
 Nil        = ValueType_(type_name='Nil',        constructor_func=None)
 Any        = ValueType_(type_name='Any',        constructor_func=None)
@@ -70,6 +139,10 @@ def decimal_ctor(*args, **kwargs):
 Error      = ValueType_(type_name='Error',      constructor_func=None)
 Image      = ValueType_(type_name='Image',      constructor_func=None)
 ValueType  = ValueType_(type_name='ValueType',  constructor_func=ValueType_)
+T          = ValueType_(type_name='T',  constructor_func=None)
+T1         = ValueType_(type_name='T1',  constructor_func=None)
+T2         = ValueType_(type_name='T2',  constructor_func=None)
+T3         = ValueType_(type_name='T3',  constructor_func=None)
 
 
 # QuantityInt= ValueType_(type_name='QuantityInt',constructor_func=None)
@@ -101,17 +174,33 @@ def decimal_ctor(*args, **kwargs):
 Query      = ValueType_(type_name='Query',      constructor_func=None) 
 DeltaQuery = ValueType_(type_name='DeltaQuery', constructor_func=None) 
 Effect     = ValueType_(type_name='Effect',     constructor_func=None) 
-DataFrame  = ValueType_(type_name='DataFrame', constructor_func=None)     
+DataFrame  = ValueType_(type_name='DataFrame',  constructor_func=None)     
 # EZefRefs   = ValueType_(type_name='EZefRefs', constructor_func=None)     
 # EZefRefss  = ValueType_(type_name='EZefRefss', constructor_func=None)     
 # ZefRefs    = ValueType_(type_name='ZefRefs', constructor_func=None)     
 # ZefRefss   = ValueType_(type_name='ZefRefss', constructor_func=None)     
 
-
-Pattern = ValueType_(type_name='Pattern', constructor_func=None)
-# These are special classes: using them with [...] returns a ValueType_ though
-Union = UnionClass()
-Intersection = IntersectionClass()
-Is = IsClass()
-SetOf = SetOfClass()
-Complement = ComplementClass()
+Pattern        = ValueType_(type_name='Pattern',             constructor_func=None)
+Union          = ValueType_(type_name='Union',               constructor_func=None,             get_item_func=union_getitem)
+Intersection   = ValueType_(type_name='Intersection',        constructor_func=None,             get_item_func=intersection_getitem)
+Is             = ValueType_(type_name='Is',                  constructor_func=None,             get_item_func=is_getitem)
+SetOf          = ValueType_(type_name='SetOf',               constructor_func=setof_ctor,       get_item_func=setof_getitem)
+Complement     = ValueType_(type_name='Complement',          constructor_func=None,             get_item_func=complement_getitem)
+RP             = ValueType_(type_name='RP',                  constructor_func=None,             get_item_func=rp_getitem)
+HasValue       = ValueType_(type_name='HasValue',            constructor_func=None)
+
+def operates_on_ctor(x):
+    from .._ops import operates_on, contains
+    return Is[operates_on | contains[x]]
+
+def related_ops_ctor(x):
+    from .._ops import related_ops, contains
+    return Is[related_ops | contains[x]]
+
+def used_for_ctor(x):
+    from .._ops import used_for, contains
+    return Is[used_for | contains[x]]
+
+OperatesOn     = ValueType_(type_name='OperatesOn',   constructor_func = operates_on_ctor,  get_item_func = operates_on_ctor)
+RelatedOps     = ValueType_(type_name='RelatedOps',   constructor_func = related_ops_ctor,  get_item_func = related_ops_ctor)
+UsedFor        = ValueType_(type_name='UsedFor',      constructor_func = used_for_ctor, get_item_func =used_for_ctor)
diff --git a/python/zef/core/VT/value_type.py b/python/zef/core/VT/value_type.py
index 46ef46b9..d0f538ce 100644
--- a/python/zef/core/VT/value_type.py
+++ b/python/zef/core/VT/value_type.py
@@ -65,22 +65,30 @@
 needed?
 """
 from ..error import Error
+from .._core import *
+from ..internals import *
+
+
 # For certain value types, the user may want to call e.g. Float(4.5).
 # This looks up the associated function 
 _value_type_constructor_funcs = {}
+_value_type_get_item_funcs = {}
 
 class ValueType_:
     """ 
     Zef ValueTypes are Values themselves.
     """
-    def __init__(self, type_name: str, absorbed=(), constructor_func=None):
+    def __init__(self, type_name: str, absorbed=(), constructor_func=None, get_item_func=None):
             self.d = {
                 'type_name': type_name,
                 'absorbed': absorbed,
             }
             if constructor_func is not None:
                 _value_type_constructor_funcs[type_name] = constructor_func
+            if get_item_func is not None:
+                _value_type_get_item_funcs[type_name] = get_item_func
 
+            self.allowed_types = (ValueType_, EntityTypeStruct, RelationTypeStruct, AtomicEntityTypeStruct, BlobTypeStruct, BlobType, AtomicEntityType, EntityType, RelationType)
         
     def __repr__(self):
         return self.d['type_name'] + (
@@ -98,7 +106,11 @@ def __call__(self, *args, **kwargs):
 
 
     def __getitem__(self, x):
-        return ValueType_(self.d["type_name"], absorbed=(*self.d['absorbed'], x))
+        try:
+            f = _value_type_get_item_funcs[self.d["type_name"]]
+            return f(x)
+        except KeyError:
+            return ValueType_(self.d["type_name"], absorbed=(*self.d['absorbed'], x))
 
 
     def __eq__(self, other):
@@ -112,105 +124,55 @@ def __hash__(self):
 
     def __or__(self, other):
         from ..op_structs import ZefOp
-        from zef.core import EntityType, RelationType, AtomicEntityType
-        if isinstance(other, (ValueType_, EntityType, RelationType, AtomicEntityType)):
+        if isinstance(other, self.allowed_types):
             return simplify_value_type(ValueType_(type_name='Union', absorbed=(self, other,)))
         elif isinstance(other, ZefOp):
             return other.__ror__(self)
         else:
             raise Exception(f'"ValueType_`s "|" called with unsupported type {type(other)}')
+
+    def __ror__(self, other):
+        # Is | commutative? Going to assume it isn't
+        if isinstance(other, self.allowed_types):
+            return simplify_value_type(ValueType_(type_name='Union', absorbed=(other, self,)))
+        else:
+            raise Exception(f'"ValueType_`s right-side "|" called with unsupported type {type(other)}')
     
     def __and__(self, other):
-        from zef.core import EntityType, RelationType, AtomicEntityType
-        if isinstance(other, (ValueType_, EntityType, RelationType, AtomicEntityType)):
+        if isinstance(other, self.allowed_types):
             return simplify_value_type(ValueType_(type_name='Intersection', absorbed=(self, other,)))
         else:
             raise Exception(f'"ValueType_`s "&" called with unsupported type {type(other)}')
 
+    def __rand__(self, other):
+        # Is & commutative? Going to assume it isn't
+        if isinstance(other, self.allowed_types):
+            return simplify_value_type(ValueType_(type_name='Intersection', absorbed=(other, self,)))
+        else:
+            raise Exception(f'"ValueType_`s right-side "&" called with unsupported type {type(other)}')
+
     def __invert__(self):
         return ValueType_(type_name='Complement', absorbed=(self,))
     
 
-
-
-class UnionClass:
-    def __getitem__(self, x):
-        from ..op_structs import ZefOp
-        if isinstance(x, tuple):
-            return ValueType_(type_name='Union', absorbed=x)
-        elif isinstance(x, ValueType_):
-            return ValueType_(type_name='Union', absorbed=(x,))
-        elif isinstance(x, ZefOp):
-            return ValueType_(type_name='Union', absorbed=(x,))
-        else:
-            raise Exception(f'"Union[...]" called with unsupported type {type(x)}')
-            
-
-class IntersectionClass:
-    def __getitem__(self, x):
-        from ..op_structs import ZefOp
-        if isinstance(x, tuple):
-            return ValueType_(type_name='Intersection', absorbed=x)
-        elif isinstance(x, ValueType_):
-            return ValueType_(type_name='Intersection', absorbed=(x,))
-        elif isinstance(x, ZefOp):
-            return ValueType_(type_name='Intersection', absorbed=(x,))
-        else:
-            raise Exception(f'"Intersection[...]" called with unsupported type {type(x)}')
-            
-
-
-class ComplementClass:
-    def __getitem__(self, x):
-        if isinstance(x, ValueType_):
-            return ValueType_(type_name='Complement', absorbed=(x,))
-        else:
-            raise Exception(f'"Complement[...]" called with unsupported type {type(x)}')
-   
-
-class IsClass:
-    def __getitem__(self, x):
-        from ..op_structs import ZefOp
-        if isinstance(x, tuple):
-            return ValueType_(type_name='Is', absorbed=x)
-        elif isinstance(x, ValueType_):
-            return ValueType_(type_name='Is', absorbed=(x,))
-        elif isinstance(x, ZefOp):
-            return ValueType_(type_name='Is', absorbed=(x,))
-        else:
-            raise Exception(f'"Is[...]" called with unsupported type {type(x)}')
-         
-
-class SetOfClass:
-    def __getitem__(self, x):
-        # TODO: make sure that x is a zef value. No other python objects that we can't serialize etc.
-        return ValueType_(type_name='SetOf', absorbed=(x, ))
-    def __call__(self, *x):
-        """
-        calling SetOf(5,6,7) is a more convenient shorthand notation than 
-        SetOf[5][6][7]. But the former expression evaluates to the latter.
-
-        We can't use `SetOf[5,6,7]` here, since Python's treatment of
-        the [...] operator converts this to a tuple `SetOf[(5,6,7)]`,
-        which itself is a valid expression.
-        """
-        return ValueType_(type_name='SetOf', absorbed = x)
-
-
-
 def make_distinct(v):
     """
     utility function to replace the 'distinct'
     zefop and preserve order.
-    Writing this on a plane and can't use zefops,
-    since I forgot the environment variable to
-    allow offline mode.
     """
     seen = set()
     for el in v:
-        if el not in seen:
+        if isinstance(el, set):
+            # we can't import the following at the top of file or even outside this if
+            # block: this function executes upon import of zef and leads to 
+            # circular dependencies. We do really want to use the value_hash zefop though.
+            from ..op_implementations.implementation_typing_functions import value_hash
+            h = value_hash(el)
+        else:
+            h = el
+        if h not in seen:
             yield el
-            seen.add(el)
+            seen.add(h)
 
 
 def simplify_value_type(x):
diff --git a/python/zef/core/__init__.py b/python/zef/core/__init__.py
index 73f607d2..bba34e76 100644
--- a/python/zef/core/__init__.py
+++ b/python/zef/core/__init__.py
@@ -58,10 +58,12 @@
 # This set of imports is to define the order. Later imports are the ones to
 # actually provide useful exports.
 from . import internals
+from . import pure_utils
 from . import error
 from . import image
 from . import op_structs
 from . import _ops
+from . import VT
 from . import zef_functions
 from . import abstract_raes
 from . import graph_delta
@@ -97,6 +99,7 @@
     List,
     Dict,
     Set,
+    Stream,
     ValueType,
     Instantiated, 
     Terminated, 
@@ -107,7 +110,13 @@
     Complement,
     Is,
     SetOf,
+    RP,
+    HasValue,
     Pattern,
+
+    RelatedOps,
+    UsedFor,
+    OperatesOn
     )
 from .VT.value_type import ValueType_
 
@@ -129,7 +138,7 @@
 
 instantiated     = make_custom_entity(name_to_display='instantiated', predetermined_uid='60252a53a03086b7')
 terminated       = make_custom_entity(name_to_display='terminated', predetermined_uid='4f676154ffeb9dc8')
-value_assigned   = make_custom_entity(name_to_display='value_assigned', predetermined_uid='c31287dab677f38c')
+assigned         = make_custom_entity(name_to_display='assigned', predetermined_uid='c31287dab677f38c')
 
 infinity           = make_custom_entity(name_to_display='infinity',    predetermined_uid='4906648460291096')
 nil                = make_custom_entity(name_to_display='nil',         predetermined_uid='1654670075329719') #| register_call_handler[f1] | run[execute] | get['entity'] | collect  # TODO
diff --git a/python/zef/core/_ops.py b/python/zef/core/_ops.py
index 181b169d..6cd20589 100644
--- a/python/zef/core/_ops.py
+++ b/python/zef/core/_ops.py
@@ -135,6 +135,7 @@ def make_zefop(rt):
 iterate         = make_zefop(RT.Iterate)
 scan            = make_zefop(RT.Scan)
 group_by        = make_zefop(RT.GroupBy)
+group           = make_zefop(RT.Group)
 transpose       = make_zefop(RT.Transpose)
 frequencies     = make_zefop(RT.Frequencies)
 max             = make_zefop(RT.Max)
@@ -153,12 +154,12 @@ def make_zefop(rt):
 take_while_pair = make_zefop(RT.TakeWhilePair)
 take_until      = make_zefop(RT.TakeUntil)                 # TODO: use 'take_until' in the RX-sense: complete the stream based on another stream emitting an item. Add 'including' to take_while as a flag for the current behavior?
 skip_while      = make_zefop(RT.SkipWhile)
-drop            = make_zefop(RT.Drop)                      # TODO: retire this and switch to skip
 skip            = make_zefop(RT.Skip)
 length          = make_zefop(RT.Length) 
 nth             = make_zefop(RT.Nth) 
 now             = make_zefop(RT.Now) 
 events          = make_zefop(RT.Events) 
+preceding_events= make_zefop(RT.PrecedingEvents) 
 to_delegate     = make_zefop(RT.ToDelegate) 
 delegate_of     = make_zefop(RT.DelegateOf) 
 target          = make_zefop(RT.Target) 
@@ -170,7 +171,6 @@ def make_zefop(rt):
 time            = make_zefop(RT.Time) 
 value           = make_zefop(RT.Value)
 sort            = make_zefop(RT.Sort)
-instances       = make_zefop(RT.Instances)
 uid             = make_zefop(RT.Uid)
 frame           = make_zefop(RT.Frame)
 discard_frame   = make_zefop(RT.DiscardFrame)
@@ -184,13 +184,14 @@ def make_zefop(rt):
 to_ezefref      = make_zefop(RT.ToEZefRef)
 root            = make_zefop(RT.Root)
 terminate       = make_zefop(RT.Terminate) 
-assign_value    = make_zefop(RT.AssignValue) 
+assign          = make_zefop(RT.Assign) 
 is_a            = make_zefop(RT.IsA)
 is_represented_as = make_zefop(RT.IsRepresentedAs)
 representation_type = make_zefop(RT.RepresentationType)
 rae_type        = make_zefop(RT.RaeType)
 abstract_type   = make_zefop(RT.AbstractType)
 fill_or_attach  = make_zefop(RT.FillOrAttach)
+set_field       = make_zefop(RT.SetField)
 Assert          = make_zefop(RT.Assert)
 allow_tombstone = make_zefop(RT.AllowTombstone)
 tag             = make_zefop(RT.Tag)
@@ -201,8 +202,6 @@ def make_zefop(rt):
 text_art        = make_zefop(RT.TextArt)
 
 sign            = make_zefop(RT.Sign)
-if_then_else    = make_zefop(RT.IfThenElse)
-if_then_else_apply    = make_zefop(RT.IfThenElseApply)
 attempt         = make_zefop(RT.Attempt)
 bypass          = make_zefop(RT.Bypass)
 pattern         = make_zefop(RT.Pattern)
@@ -213,12 +212,14 @@ def make_zefop(rt):
 is_distinct_by  = make_zefop(RT.IsDistinctBy)
 shuffle         = make_zefop(RT.Shuffle)
 split           = make_zefop(RT.Split)
+split_left      = make_zefop(RT.SplitLeft)
+split_right     = make_zefop(RT.SplitRight)
 split_if        = make_zefop(RT.SplitIf)
 graphviz        = make_zefop(RT.Graphviz)
 
-tx              = make_zefop(RT.Tx)
-schema          = make_zefop(RT.Schema)
+blueprint       = make_zefop(RT.Blueprint)
 exists_at       = make_zefop(RT.ExistsAt)
+aware_of        = make_zefop(RT.AwareOf)
 base_uid        = make_zefop(RT.BaseUid)
 origin_uid      = make_zefop(RT.OriginUid)
 origin_rae      = make_zefop(RT.OriginRAE)
@@ -226,8 +227,6 @@ def make_zefop(rt):
 has_out         = make_zefop(RT.HasOut)                # z1 | has_out[RT.Foo] use  (z1, RT.Foo, Z) | exists  /   (z, RT.Foo, RAE) | exists[g]  /   (z, RT.Foo, RAE) | exists[now(g)][single]
 has_in          = make_zefop(RT.HasIn)                 # z1 | has_in[RT.Foo]  use  (Z, RT.Foo, z1) | exists
 
-All             = make_zefop(RT.All)               # TODO: retire
-
 In              = make_zefop(RT.In)
 Ins             = make_zefop(RT.Ins)
 Out             = make_zefop(RT.Out)
@@ -239,22 +238,18 @@ def make_zefop(rt):
 
 
 is_zefref_promotable= make_zefop(RT.IsZefRefPromotable)  # Retire this. this is a old love level operator. We can use is_a[RAE] or an extended concept new.
-ins             = make_zefop(RT.InsOld)                   # z < L[RT]
-outs            = make_zefop(RT.OutsOld)                  # z > L[RT]
-ins_and_outs    = make_zefop(RT.InsAndOutsOld)            # we don't seem to use this very much (looking at existing code base).
-time_slice      = make_zefop(RT.TimeSlice)             # with the GraphSlice syntax it seems that we have been using time slices / txs mostly as a proxy for that. Given the data oriented approach, it seems a more consistent design to just use Int instead of time slices when we actually want a number?
+time_slice      = make_zefop(RT.TimeSlice)        
+graph_slice_index=make_zefop(RT.GraphSliceIndex) 
     
 instantiation_tx= make_zefop(RT.InstantiationTx)       # use tx[instantiated]
 termination_tx  = make_zefop(RT.TerminationTx)         # use tx[terminated]   
-relations       = make_zefop(RT.Relations)             # g | now | All[(z1, RT.Bar, z2)]   with pattern matching style any of the three args can also be replaced with a more general class
+relations       = make_zefop(RT.Relations)             # g | now | all[(z1, RT.Bar, z2)]   with pattern matching style any of the three args can also be replaced with a more general class
 relation        = make_zefop(RT.Relation)              # looking through our code base for use cases of this op, I don't think a separate operator is necessary. Just use the syntax above followed by ... | single. If required more often, it is much easier to add this in future than to remove it.
-call            = make_zefop(RT.Call)                  # useful now. But the "func[...]" syntax could replace this in future?
-unpack = make_zefop(RT.Unpack)
+unpack          = make_zefop(RT.Unpack)
 _any            = make_zefop(RT._Any)                  # used as a wildcard
 has_relation    = make_zefop(RT.HasRelation)     
 
 replace_at      = make_zefop(RT.ReplaceAt)           
-pad_to_length   = make_zefop(RT.PadToLength)           
 pad_left        = make_zefop(RT.PadLeft)           
 pad_right       = make_zefop(RT.PadRight)           
 pad_center      = make_zefop(RT.PadCenter)           
@@ -304,8 +299,9 @@ def make_zefop(rt):
 make_request  = make_zefop(RT.MakeRequest)
 
 
-zascii_to_asg    = make_zefop(RT.ZasciiToAsg)
-zascii_to_schema = make_zefop(RT.ZasciiToSchema)
+zascii_to_asg       = make_zefop(RT.ZasciiToAsg)
+zascii_to_flatgraph = make_zefop(RT.ZasciiToFlatGraph)
+zascii_to_blueprint_fg = make_zefop(RT.ZasciiToBlueprintFg)
 
                 # Syntax????????????????? 
                 # has_relation(z1, RT.Foo, z2)      replaced by 
@@ -314,7 +310,7 @@ def make_zefop(rt):
                 #       2) g_slice | contains[(z1, RT.Foo, z2)]
 # Syntax choices:   
 #       exists or contained_in?
-#       All or instances?    Also: my_delegate | All    or my_delegate | instances?
+#       All or instances?    Also: my_delegate | all    or my_delegate | instances?
 
 
 
@@ -323,6 +319,7 @@ def make_zefop(rt):
 merge           = make_zefop(RT.Merge)                 # We need this for observables. Only there?
 
 blake3          = make_zefop(RT.Blake3)
+value_hash      = make_zefop(RT.ValueHash)        
 to_zef_list     = make_zefop(RT.ToZefList)
 transact        = make_zefop(RT.Transact)
 # transact
@@ -338,7 +335,6 @@ def make_zefop(rt):
 
 peel            = make_zefop(RT.Peel)                
 match           = make_zefop(RT.Match)                
-match_apply     = make_zefop(RT.MatchApply)                
 Range           = make_zefop(RT.Range)      
 zstandard_compress = make_zefop(RT.ZstandardCompress)
 zstandard_decompress = make_zefop(RT.ZstandardDecompress)
@@ -349,7 +345,20 @@ def make_zefop(rt):
 is_between      = make_zefop(RT.IsBetween)
 If              = make_zefop(RT.If)
 
+field           = make_zefop(RT.Field)
+fields          = make_zefop(RT.Fields)
+apply           = make_zefop(RT.Apply)
+split_on_next   = make_zefop(RT.SplitOnNext)
+indexes_of      = make_zefop(RT.IndexesOf)
+
+
 
+examples        = make_zefop(RT.Examples)
+signature       = make_zefop(RT.Signature)
+tags            = make_zefop(RT.Tags)
+related_ops     = make_zefop(RT.RelatedOps)
+operates_on     = make_zefop(RT.OperatesOn)
+used_for        = make_zefop(RT.UsedFor)
 
 
 # TODO: implement
@@ -369,3 +378,22 @@ def make_zefop(rt):
 # window(Max[10], Max[30/sec, over[2*sec]])
 # 
 # time_travel         # RuntimeError: Only(EZefRefs zs) request, but length was 0
+
+
+
+
+
+# -------- These are not ZefOps, but using the `.` operator, they return ZefOps.
+# The user may not even be aware of this distinction and therefore this namespace 
+# is the most natural to put them in.
+class FClass:    
+    def __getattr__(self, s: str):        
+        return field[RT(s)]    # just returns a normal zefop called 'field'
+
+F = FClass()
+
+class FsClass:    
+    def __getattr__(self, s: str):        
+        return fields[RT(s)]    # just returns a normal zefop called 'field'
+
+Fs = FsClass()
diff --git a/python/zef/core/abstract_raes.py b/python/zef/core/abstract_raes.py
index 98391a60..701507f2 100644
--- a/python/zef/core/abstract_raes.py
+++ b/python/zef/core/abstract_raes.py
@@ -149,8 +149,8 @@ def __eq__(self, other):
         return self.d['type'] == other.d['type'] and self.d['uid'] == other.d['uid']
 
     def __le__(self, value):
-        from ._ops import assign_value
-        return self | assign_value[value]
+        from ._ops import assign
+        return self | assign[value]
     
     def __hash__(self):
         return hash(self.d['uid'])
diff --git a/python/zef/core/error.py b/python/zef/core/error.py
index 9723dc8f..4ad31ae2 100644
--- a/python/zef/core/error.py
+++ b/python/zef/core/error.py
@@ -31,6 +31,9 @@ def __repr__(self):
     def __eq__(self, other):
         if not isinstance(other, _ErrorType): return False
         return self.name == other.name and self.args == other.args
+
+    def __bool__(self):
+        return False
     
 
 class _Error:
diff --git a/python/zef/core/fx/__init__.py b/python/zef/core/fx/__init__.py
index e4b94d8e..db51de5a 100644
--- a/python/zef/core/fx/__init__.py
+++ b/python/zef/core/fx/__init__.py
@@ -16,10 +16,6 @@
 
 from .state import _state
 
-from .graph_transactions import (
-    graph_transaction_handler,
-)
-
 
 # from .http import (
 #     http_start_server_handler,
@@ -86,6 +82,7 @@
 from .graph import (
     graph_take_transactor_role_handler,
     graph_release_transactor_role_handler,
+    graph_transaction_handler
 )
 
 
@@ -104,8 +101,6 @@
 
 # note the ".d" to access the tuple of Strings!
 _effect_handlers = {
-    FX.TX.Transact.d: graph_transaction_handler,
-    
     FX.HTTP.StartServer.d: http_start_server_handler,
     FX.HTTP.StopServer.d: http_stop_server_handler,
     FX.HTTP.SendResponse.d: http_send_response_handler,
@@ -140,7 +135,9 @@
     FX.Graph.Sync.d: graph_sync_handler,
     FX.Graph.TakeTransactorRole.d: graph_take_transactor_role_handler,
     FX.Graph.ReleaseTransactorRole.d: graph_release_transactor_role_handler,
+    FX.Graph.Transact.d: graph_transaction_handler,
 
+    
     FX.Clipboard.CopyTo.d: clipboard_copy_to_handler,
     FX.Clipboard.CopyFrom.d: clipboard_copy_from_handler,
 
diff --git a/python/zef/core/fx/clipboard.py b/python/zef/core/fx/clipboard.py
index 315b1c3d..43a7c004 100644
--- a/python/zef/core/fx/clipboard.py
+++ b/python/zef/core/fx/clipboard.py
@@ -12,7 +12,6 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from .fx_types import Effect
 
 
 # taken from zef function
@@ -69,16 +68,18 @@
 # to_clipboard = ToClipboard()
 
 
+from .fx_types import Effect
+from ..error import Error
 
 
-def clipboard_copy_to_handler(eff: Effect):
+def clipboard_copy_to_handler(eff: dict):
     """ 
     sample effect:
     {
         'type': FX.Clipboard.CopyTo,
         'value': 'hello',
     }
-    """
+    """    
     try:
         import pyperclip
     except:
@@ -87,25 +88,40 @@ def clipboard_copy_to_handler(eff: Effect):
         clipboard functionality. E.g. 'pip3 install pyperclip'
         """
         raise RuntimeError(err)
-    val = eff.d['value']
-    if type(val) in {str, float, int, bool}:
-        pyperclip.copy(str(val))
-    else:
-        raise RuntimeError(f"copying a type type(val)={type(val)} to the clipboard is not supported. Value: {val}")
-    return {}
+    try:
+        val = eff['value']
+        if type(val) in {str, float, int, bool}:
+            pyperclip.copy(str(val))
+        else:
+            raise RuntimeError(f"copying a type type(val)={type(val)} to the clipboard is not supported. Value: {val}")
+        return {}
+    except Exception as e:
+        return Error(f'executing FX.Clipboard.CopyTo for effect {eff}:\n{repr(e)}')
+
 
 
 
-def clipboard_copy_from_handler(eff: Effect):
+
+def clipboard_copy_from_handler(eff: dict):
     """ 
     For now with pyperclip, images can't be pasted
     {
         'type': FX.Clipboard.CopyFrom,
     }
     """
-    import pyperclip
-    val = pyperclip.paste()
-    return {
-        'value': val
-    }
+    try:
+        import pyperclip
+    except:
+        err = """
+        please install pyperclip if you want to use the copy to/from
+        clipboard functionality. E.g. 'pip3 install pyperclip'
+        """
+        raise RuntimeError(err)
+
+    try:
+        val = pyperclip.paste()
+        return {'value': val}
+    except Exception as e:
+        return Error(f'executing FX.Clipboard.CopyFrom for effect {eff}:\n{repr(e)}')
+
 
diff --git a/python/zef/core/fx/fx_types.py b/python/zef/core/fx/fx_types.py
index 9bb2934f..52ff67a6 100644
--- a/python/zef/core/fx/fx_types.py
+++ b/python/zef/core/fx/fx_types.py
@@ -50,11 +50,6 @@ def __call__(self, **kwargs):
 
 
 
-class _TX_Class():
-    _name = "TX"
-    Transact = FXElement(('TX', 'Transact'))
-
-
 class _HTTP_Class():
     _name = "HTTP"
     StartServer = FXElement(('HTTP', 'StartServer'))
@@ -116,6 +111,7 @@ class _Graph_Class():
     Sync = FXElement(('Graph', 'Sync'))
     TakeTransactorRole = FXElement(('Graph', 'TakeTransactorRole'))
     ReleaseTransactorRole = FXElement(('Graph', 'ReleaseTransactorRole'))
+    Transact = FXElement(('Graph', 'Transact'))
     # Or does it make more sense to write FX.Tag.Graph? Tagging of RAEs definitely belongs into a GraphDelta / graph tx though
 
 class _Stream_Class():
@@ -163,7 +159,6 @@ class _RAE_Class():
 
 
 class _FX_Class:
-    TX = _TX_Class()
     HTTP = _HTTP_Class()
     Websocket = _Websocket_Class()
     Subprocess = _Subprocess_Class()
@@ -182,48 +177,14 @@ class _FX_Class:
 
 FX = _FX_Class()
 
-_group_types = [ _Clipboard_Class,_FX_Class,_GraphQL_Class,_Graph_Class,_HTTP_Class,_LocalFile_Class,_Privileges_Class,_S3_Class,_Stream_Class,_TX_Class,_Websocket_Class,_ZefHub_Class,_ZefUI_Class]
+_group_types = [ _Clipboard_Class,_FX_Class,_GraphQL_Class,_Graph_Class,_HTTP_Class,_LocalFile_Class,_Privileges_Class,_S3_Class,_Stream_Class,_Websocket_Class,_ZefHub_Class,_ZefUI_Class]
 #---------------------------------------------------------------------------------------
-
-_warn_did_not_run = True
-
-class _Effect_Class:
-    def __init__(self, d: dict):
-        # TODO: Add checks that this is a valid effect
-        self.d = d
-        self._been_run = False
-        
-    def __repr__(self):
-        return f"Effect({self.d})"
-
-    def __del__(self):
-        if not _warn_did_not_run:
-            return
-        if not self._been_run:
-            import logging
-            # import traceback
-            # traceback.print_stack()
-            logging.warning(f"Effect of type {self.d.get('type')} was garbage collected without being run. Set {__name__}._warn_did_not_run to False to disable this warning.")
-
-    def __call__(self):
-        raise TypeError("Effects are not callable. Please use `eff | run` instead.")
-
-    def __iter__(self):
-        """ 
-        Allow casting to dictionaries: just return the internal
-        dictionary. An effect is a thin wrapper around a dictionary
-        with the constructor allowing initial checking of requirements.
-        
-        Equivalent to "peel"
-        """
-        return iter(self.d.items())
-
     
 
 # just a function, since Effect will just become a value.
 # same syntax as a regular constructor and equivalent to to_effect.
 # The latter is the more usual option to use in the context of piping
-def Effect(*args, **kwargs):
+def Effect(*args, **kwargs) -> dict:
     """
     Effects can be constructed via keyword arguments
     or by passing in a dictionary.
@@ -236,8 +197,8 @@ def Effect(*args, **kwargs):
         assert len(kwargs) == 0
         assert len(args) == 1
         assert isinstance(args[0], dict)
-        return _Effect_Class(args[0])
+        return args[0]
 
     else:
-        return _Effect_Class(kwargs)
+        return kwargs
 
diff --git a/python/zef/core/fx/graph.py b/python/zef/core/fx/graph.py
index dabb95bc..774e040e 100644
--- a/python/zef/core/fx/graph.py
+++ b/python/zef/core/fx/graph.py
@@ -14,12 +14,39 @@
 
 from .fx_types import Effect
 
-def graph_take_transactor_role_handler(eff: Effect):
+def graph_take_transactor_role_handler(eff: dict):
     from ...pyzef.main import make_primary
-    make_primary(eff.d["graph"], True)
+    make_primary(eff["graph"], True)
     return {}
 
-def graph_release_transactor_role_handler(eff: Effect):
+def graph_release_transactor_role_handler(eff: dict):
     from ...pyzef.main import make_primary
-    make_primary(eff.d["graph"], False)
-    return {}
\ No newline at end of file
+    make_primary(eff["graph"], False)
+    return {}
+
+def graph_transaction_handler(eff: dict):
+    """[summary]
+
+    Args:
+        eff (dict): {
+            "type": FX.Graph.Transact,
+            "target_graph": g1,
+            "commands": list_of_commands
+        }
+    """
+
+    from ..graph_delta import perform_transaction_commands, filter_temporary_ids, unpack_receipt
+
+    if eff["target_graph"].graph_data.is_primary_instance:
+        receipt = perform_transaction_commands(eff['commands'], eff['target_graph'])
+    else:
+        from ..overrides import merge
+        receipt = merge(eff["commands"], eff["target_graph"])
+    
+    # we need to forward this if it is in the effect
+    if 'unpacking_template' in eff:
+        return unpack_receipt(eff['unpacking_template'], receipt)
+
+    receipt = filter_temporary_ids(receipt)
+    return receipt
+    
\ No newline at end of file
diff --git a/python/zef/core/fx/graph_transactions.py b/python/zef/core/fx/graph_transactions.py
deleted file mode 100644
index f35d7fa9..00000000
--- a/python/zef/core/fx/graph_transactions.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# Copyright 2022 Synchronous Technologies Pte Ltd
-#
-# Licensed under the Apache License, Version 2.0 (the "License");
-# you may not use this file except in compliance with the License.
-# You may obtain a copy of the License at
-#
-#     http://www.apache.org/licenses/LICENSE-2.0
-#
-# Unless required by applicable law or agreed to in writing, software
-# distributed under the License is distributed on an "AS IS" BASIS,
-# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-# See the License for the specific language governing permissions and
-# limitations under the License.
-
-from .fx_types import Effect
-
-
-def graph_transaction_handler(eff: Effect):
-    """[summary]
-
-    Args:
-        eff (Effect): Effect({
-            "type": FX.TX.Transact,
-            "target_graph": g1,
-            "commands": list_of_commands
-        })
-    """
-
-    from ..graph_delta import perform_transaction_commands, filter_temporary_ids, unpack_receipt
-
-    if eff.d["target_graph"].graph_data.is_primary_instance:
-        receipt = perform_transaction_commands(eff.d['commands'], eff.d['target_graph'])
-    else:
-        from ..overrides import merge
-        receipt = merge(eff.d["commands"], eff.d["target_graph"])
-    
-    # we need to forward this if it is in the effect
-    if 'unpacking_template' in eff.d:
-        return unpack_receipt(eff.d['unpacking_template'], receipt)
-
-    receipt = filter_temporary_ids(receipt)
-    return receipt
\ No newline at end of file
diff --git a/python/zef/core/fx/graphql.py b/python/zef/core/fx/graphql.py
index 06472496..1a29929a 100644
--- a/python/zef/core/fx/graphql.py
+++ b/python/zef/core/fx/graphql.py
@@ -26,25 +26,24 @@
 
 def graphql_start_server_handler(eff: Effect):
     """Example
-    Effect({
+    {
         "type": FX.GraphQL.StartServer,
         "port": 5001, (default=5000) 
         "schema_root": z_schema,
         "path": "/gql", (default="/gql")
         "playground_path": "/", (default=None)
         "open_browser": False (default=False)
-    }) | run
+    } | run
     """
 
-    eff_d = eff.d
 
     from ...gql.generate_gql_api import make_api
-    schema = make_api(eff_d.pop('schema_root'))
-    port = eff_d.pop("port", 5000)
+    schema = make_api(eff['schema_root'])
+    port = eff.get("port", 5000)
 
-    gql_path = eff_d.pop("path", "/gql")
-    playground_path = eff_d.pop("playground_path", None)
-    open_browser = eff_d.pop("open_browser", False)
+    gql_path = eff.get("path", "/gql")
+    playground_path = eff.get("playground_path", None)
+    open_browser = eff.get("open_browser", False)
 
     if not gql_path.startswith("/"):
         raise Exception("GQL path must start with /")
@@ -64,13 +63,14 @@ def resolve_gql_query(req, port=port, playground_path=playground_path, gql_path=
             body = body.replace("ZEF_GQL_PATH", gql_path)
             req["response_body"] = body
         elif req["path"] == gql_path:
-            success,result = graphql_sync(schema, json.loads(req["request_body"]))
+            success, result = graphql_sync(schema, json.loads(req["request_body"]))
             req["response_body"] = json.dumps(result)
         return req
 
-    logging = eff_d.pop("logging", False)
+
+    logging = eff.get("logging", False)
     http_r = Effect({
-        **eff_d,
+        **eff,     # Verify that this is ok after change: the dict was mutated previously (using pop). There may be more key-value pairs in there now.
         'type': FX.HTTP.StartServer,
         'port': port,
         # 'pipe_into': map[resolve_gql_query] | subscribe[run]
@@ -91,13 +91,11 @@ def resolve_gql_query(req, port=port, playground_path=playground_path, gql_path=
     return http_r
     
     
-def graphql_stop_server_handler(eff: Effect):
-    new_d = dict(eff.d)
-    new_d["type"] = FX.HTTP.StopServer
-    return run(Effect(new_d))
+def graphql_stop_server_handler(eff: dict):    
+    return run({**eff, 'type': FX.HTTP.StopServer})
 
     
-def graphql_start_playground_handler(eff: Effect):
+def graphql_start_playground_handler(eff: dict):
     """Example
     Effect({
         "type": FX.GraphQL.StartPlayground,
@@ -107,34 +105,31 @@ def graphql_start_playground_handler(eff: Effect):
         "playground_path": "/", (default="/")
         "open_browser": True (default=True)
     }) | run
-    """
-    
-    schema = eff.d['schema_root']
-    port = eff.d.get("port", 5000)
+    """   
+    schema = eff['schema_root']
+    port = eff.get("port", 5000)
 
-    gql_path = eff.d.get("path", "/gql")
-    playground_path = eff.d.get("playground_path", "/") # It is required!
-    open_browser = eff.d.get("open_browser", True)
+    gql_path = eff.get("path", "/gql")
+    playground_path = eff.get("playground_path", "/") # It is required!
+    open_browser = eff.get("open_browser", True)
 
     if not gql_path.startswith("/"):
         raise Exception("GQL path must start with /")
     if not playground_path.startswith("/"):
         raise Exception("Playground path must start with /")
 
-    http_r = Effect({
+    http_r = {
         "type": FX.GraphQL.StartServer,
         "port": port,
         "schema_root": schema,
         "path": gql_path,
         "playground_path": playground_path,
         "open_browser": open_browser,
-    }) | run
-
+    } | run
+    
     return http_r
     
     
-def graphql_stop_playground_handler(eff: Effect):
-    new_d = dict(eff.d)
-    new_d["type"] = FX.HTTP.StopServer
-    return run(Effect(new_d))
+def graphql_stop_playground_handler(eff: dict):    
+    return run({**eff, 'type': FX.HTTP.StopServer})
     
diff --git a/python/zef/core/fx/http.py b/python/zef/core/fx/http.py
index c300e31d..01f953f8 100644
--- a/python/zef/core/fx/http.py
+++ b/python/zef/core/fx/http.py
@@ -14,7 +14,7 @@
 
 
 #=============================================================================== HTTP Server handlers =========================================================================================================
-from .fx_types import Effect, FX, _Effect_Class
+from .fx_types import Effect, FX
 from uuid import uuid4
 from .._ops import *
 from ..error import Error
@@ -107,11 +107,11 @@ def log_message(self, *args, **kwds):
 
 
 
-def http_start_server_handler(eff: Effect):
+def http_start_server_handler(eff: dict):
     """[summary]
 
     Args:
-        eff (Effect): {
+        eff : {
             "type": FX.HTTP.StartServer,
             "port": 5000, (default = 5000)
             "bind_address": "0.0.0.0", (default = "localhost")
@@ -127,20 +127,22 @@ def http_start_server_handler(eff: Effect):
     pushable_stream = None
     server = None
     thread = None
-
     try:
         open_requests = {}
         pushable_stream = Effect({'type': FX.Stream.CreatePushableStream}) | run
         server_uuid = str(uid(pushable_stream.stream_ezefref))
-        if eff.d['pipe_into'] is not None: pushable_stream | eff.d['pipe_into']
-        port = eff.d.get('port', 5000)
-        bind_address = eff.d.get('bind_address', "localhost")
-        do_logging = eff.d.get('logging', True)
-
-        zef_locals = {"open_requests": open_requests,
-                    "stream" : pushable_stream,
-                    "server_uuid": server_uuid,
-                    "port": port}
+        if eff.get('pipe_into', None) is not None:
+            pushable_stream | eff['pipe_into']
+        port = eff.get('port', 5000)
+        bind_address = eff.get('bind_address', "localhost")
+        do_logging = eff.get('logging', True)
+
+        zef_locals = {
+            "open_requests": open_requests,
+            "stream" : pushable_stream,
+            "server_uuid": server_uuid,
+            "port": port
+        }
 
         server = OurHTTPServer((bind_address, port), Handler, do_logging=do_logging)
         zef_locals["server"] = server
@@ -157,23 +159,20 @@ def http_start_server_handler(eff: Effect):
         log.debug(f"http_server started", uuid=server_uuid, port=port, bind_address=bind_address)
         return zef_locals
     except Exception as exc:
-        # Cleanup
-        # TODO:
-
-        return Error(exc)
+        raise RuntimeError(f'Error starting HTTP server: in FX.HTTP.StartServer handler: {exc}')
         
 
 
-def http_stop_server_handler(eff: Effect):
-    d = _effects_processes[eff.d["server_uuid"]]
+def http_stop_server_handler(eff: dict):
+    d = _effects_processes[eff["server_uuid"]]
     d["server"].shutdown()
     d["server"].server_close()
     d["thread"].join()
-    log.debug("http server stopped", uuid=eff.d["server_uuid"])
+    log.debug("http server stopped", uuid=eff["server_uuid"])
     return {}
     
 
-def http_send_request_handler(eff: Effect):
+def http_send_request_handler(eff: dict):
     """[summary]
 
     Args:
@@ -187,10 +186,10 @@ def http_send_request_handler(eff: Effect):
     """
     # TODO needs more testing and improvements
     import requests
-    request_url = eff.d['url']
-    request_method = eff.d.get('method', 'GET')
-    request_data = eff.d.get('data', {})
-    request_params = eff.d.get('params', {})
+    request_url = eff['url']
+    request_method = eff.get('method', 'GET')
+    request_data = eff.get('data', {})
+    request_params = eff.get('params', {})
 
     method_dict =  {'GET': requests.get, 'POST': requests.post, 'PUT': requests.put, 'DELETE': requests.delete, 'HEAD': requests.head}
     request_obj = method_dict[request_method]
@@ -199,21 +198,23 @@ def http_send_request_handler(eff: Effect):
     return {"response_text": response.text, "response_status": response.status_code}
     
 def http_send_response_handler(eff: Effect):
-    d = eff.d
+    d = eff
     if "server_uuid" not in d:
         # TODO: register this on ProcessLogger
         return Error(f"An FX.HTTP.SendResponse event must contain a 'server_uuid' field. This was not the case for eff={eff}")
 
-    d = _effects_processes[eff.d["server_uuid"]]
-    future = d["open_requests"][eff.d["request_id"]]
+    d = _effects_processes[eff["server_uuid"]]
+    future = d["open_requests"][eff["request_id"]]
 
-    msg = eff.d.get("response", "")
+    if "response" not in eff:
+        print(f"Warning: FX.HTTP.SendResponse wish did not contain 'response' field. This is probably an error. Received wish: {eff}")
+    msg = eff.get("response", "")
     assert msg is None or isinstance(msg, str) or isinstance(msg, bytes)
     if isinstance(msg, str):
         msg = msg.encode("utf-8")
 
-    status = eff.d.get("status", 200)
-    headers = eff.d.get("headers", {})
+    status = eff.get("status", 200)
+    headers = eff.get("headers", {})
     import copy
     headers = copy.deepcopy(headers)
 
@@ -316,9 +317,8 @@ def middleware_worker(req, mw):
             return None
         if isinstance(cur, dict):
             continue
-        # Why the hell is an Effect actually _Effect_Class??
         # if isinstance(cur, Effect):
-        if isinstance(cur, _Effect_Class):
+        if isinstance(cur, dict):
             return cur
 
         raise Exception(f"Unusual type in middleware_runner: {type(func)}")
diff --git a/python/zef/core/fx/local_file.py b/python/zef/core/fx/local_file.py
index dfd89a6f..2a808d36 100644
--- a/python/zef/core/fx/local_file.py
+++ b/python/zef/core/fx/local_file.py
@@ -24,39 +24,75 @@
 #TODO Docstring!
 
 def read_localfile_handler(eff: Effect):
-    filename  = eff.d["filename"]
+    """
+    Unopinionated reading of file. Returns a Bytes object.
+    >>> FX.LocalFile.Read(filename='my_file.txt')
 
-    f = open(filename, "r")
-    f = f.read()
-    return {"content": f}
+    Response example:
+    {
+        'content': some_dict,
+    }
+    """
+    try:
+        filename  = eff["filename"]
+        f = open(filename, "r")
+        f = f.read()
+        return {"content": f}
+    except Exception as e:
+        raise RuntimeError(f"Error reading file in FX.LocalFile.Read for effect={eff}:\n {repr(e)}")
 
 
 def load_localfile_handler(eff: Effect):
-    filename  = eff.d["filename"]
-    settings  = eff.d.get("settings", {})
-    format    = eff.d.get("format", None)
-
-    if not format:
-        if "." not in filename: return Error.ValueError("filename is missing an extension and a format wasn't provided!", filename)
-        else: format = filename[filename.rindex(".") + 1:]
-    elif "." not in filename: filename = filename + f".{format}"
-
-    f = open(filename, "r")
-    content = f.read()
+    """
+    Opinionated loading of file: it parses based on the file extension.
+    If you want plain reading of a file, use 
+    >>> FX.LocalFile.Load(filename='my_file.json')
+
+    Supported formats:
+    - json
+    - csv
+    - toml
+    - yaml / yml
+    - svg
+    - png
+    - jpg
+    - jpeg
+
+    Response example:
+    {
+        'content': some_dict,
+        'format': 'json',
+    }
+    """
+    try:
+        filename  = eff["filename"]
+        settings  = eff.get("settings", {})
+        format    = eff.get("format", None)
+
+        if not format:
+            if "." not in filename: return Error.ValueError("filename is missing an extension and a format wasn't provided!", filename)
+            else: format = filename[filename.rindex(".") + 1:]
+        elif "." not in filename: filename = filename + f".{format}"
+
+        f = open(filename, "r")
+        content = f.read()
+
+        if format in {"svg", "png", "jpg", "jpeg"}:
+            content = bytes(content, "UTF-8")
+            content = Image(content, format)
+        elif format in {"yaml", "yml"}:
+            content = yaml.safe_load(content)
+        elif format == "toml":
+            content = toml.loads(content)
+        elif format == "csv":
+            content = pd.read_csv(io.StringIO(content), **settings)
+        elif format == "json":
+            content = json.loads(content)
 
-    if format in {"svg", "png", "jpg", "jpeg"}:
-        content = bytes(content, "UTF-8")
-        content = Image(content, format)
-    elif format in {"yaml", "yml"}:
-        content = yaml.safe_load(content)
-    elif format == "toml":
-        content = toml.loads(content)
-    elif format == "csv":
-        content = pd.read_csv(io.StringIO(content), **settings)
-    elif format == "json":
-        content = json.loads(content)
+        return {"content": content, "format": format}
+    except Exception as e:
+        return Error(f'executing FX.LocalFile.Load for effect {eff}:\n{repr(e)}')
 
-    return {"content": content, "format": format}
 
 
 def save_localfile_handler(eff: Effect):
@@ -70,35 +106,37 @@ def save_localfile_handler(eff: Effect):
         'settings': settings,
     })
     """
-
-    content   = eff.d["content"]
-    filename  = eff.d["filename"]
-    settings  = eff.d.get("settings", {})
-
-    if isinstance(content, Image):
-        format = f'.{content.format}'
-        if format not in {".svg", ".png", ".jpg", ".jpeg"}: return Error.ValueError(f'Image format needs to be one of these types {{".svg", ".png", ".jpg", ".jpeg"}} got {format} instead.')        
-        filename = filename if format == (filename[-4:] or filename[-5:])  else filename + format
-        import zstd
-        with open(filename, 'wb') as f: f.write(zstd.decompress(content.buffer))
-        
-    elif "." in filename:
-        format = filename[filename.rindex(".") + 1:]
-        if format in {"yaml", "yml"}:
-            with open(filename, 'w') as f: f.write(yaml.safe_dump(content))
-        elif format == "toml":
-            with open(filename, 'w') as f: f.write(toml.dumps(content))
-        elif format == "csv":
-            with open(filename, 'w') as f: f.write(content.to_csv(**settings))
-        elif format == "json":
-            with open(filename, 'w') as f: f.write(json.dumps(content))
+    try:
+        content   = eff["content"]
+        filename  = eff["filename"]
+        settings  = eff.get("settings", {})
+
+        if isinstance(content, Image):
+            format = f'.{content.format}'
+            if format not in {".svg", ".png", ".jpg", ".jpeg"}: return Error.ValueError(f'Image format needs to be one of these types {{".svg", ".png", ".jpg", ".jpeg"}} got {format} instead.')        
+            filename = filename if format == (filename[-4:] or filename[-5:])  else filename + format
+            import zstd
+            with open(filename, 'wb') as f: f.write(zstd.decompress(content.buffer))
+            
+        elif "." in filename:
+            format = filename[filename.rindex(".") + 1:]
+            if format in {"yaml", "yml"}:
+                with open(filename, 'w') as f: f.write(yaml.safe_dump(content))
+            elif format == "toml":
+                with open(filename, 'w') as f: f.write(toml.dumps(content))
+            elif format == "csv":
+                with open(filename, 'w') as f: f.write(content.to_csv(**settings))
+            elif format == "json":
+                with open(filename, 'w') as f: f.write(json.dumps(content))
+            else:
+                return Error.NotImplementedError(f'Unsupported writing to file type {format}!')
         else:
-            return Error.NotImplementedError(f'Unsupported writing to file type {format}!')
-    else:
-        filename += ".txt"
-        with open(filename, 'w') as f: f.write(content)
+            filename += ".txt"
+            with open(filename, 'w') as f: f.write(content)
 
-    return {"filename": filename}
+        return {"filename": filename}
+    except Exception as e:
+        return Error(f'executing FX.LocalFile.Save for effect {eff}: {repr(e)}')
 
 
 def write_localfile_handler(eff: Effect):
@@ -111,13 +149,14 @@ def write_localfile_handler(eff: Effect):
         'content':  content,
     })
     """
-
-    content   = eff.d["content"]
-    filename  = eff.d["filename"]
+    content   = eff["content"]
+    filename  = eff["filename"]
 
     with open(filename, "w") as file: file.write(content)
     return {"filename": filename}
 
+
+
 def system_open_with_handler(eff: Effect):
     """
     This will open the file using the system's default handler application.
@@ -130,11 +169,15 @@ def system_open_with_handler(eff: Effect):
             'filepath': fname,
     })
     """
+    try:
+        import subprocess, os, platform
+        if platform.system() == 'Darwin':       # macOS
+            subprocess.Popen(('open', eff['filepath']))
+        elif platform.system() == 'Windows':    # Windows
+            os.startfile(eff['filepath'])
+        else:                                   # linux variants
+            subprocess.Popen(('xdg-open', eff['filepath']))
+
+    except Exception as e:
+        return Error(f'executing FX.LocalFile.SystemOpenWith for effect {eff}:\n{repr(e)}')
 
-    import subprocess, os, platform
-    if platform.system() == 'Darwin':       # macOS
-        subprocess.Popen(('open', eff.d['filepath']))
-    elif platform.system() == 'Windows':    # Windows
-        os.startfile(eff.d['filepath'])
-    else:                                   # linux variants
-        subprocess.Popen(('xdg-open', eff.d['filepath']))
\ No newline at end of file
diff --git a/python/zef/core/fx/privileges.py b/python/zef/core/fx/privileges.py
index 7d5f0a4e..5f92b6eb 100644
--- a/python/zef/core/fx/privileges.py
+++ b/python/zef/core/fx/privileges.py
@@ -14,8 +14,6 @@
 
 from .._core import *
 from .._ops import *
-from .fx_types import _Effect_Class
-
 from ...pyzef.admins import add_right, remove_right
 
 def main_implementation(privilege, target, action, user):
@@ -29,7 +27,7 @@ def main_implementation(privilege, target, action, user):
         raise NotImplementedError("TODO: handle targets other than graphs")
 
     assert isinstance(user, str), "Users need to be strings at the moment"
-    # This is to allow groups as well as users but is only temrporary while we're using strings.
+    # This is to allow groups as well as users but is only temporary while we're using strings.
     if ':' not in user:
         s_subject = "user:" + user
     else:
@@ -56,32 +54,32 @@ def main_implementation(privilege, target, action, user):
 
     return ret
 
-def privileges_grantview_handler(eff: _Effect_Class):
-    return main_implementation(KW.view, eff.d["target"], KW.grant, eff.d["user"])
+def privileges_grantview_handler(eff: dict):
+    return main_implementation(KW.view, eff["target"], KW.grant, eff["user"])
 
-def privileges_grantappend_handler(eff: _Effect_Class):
-    return main_implementation(KW.append, eff.d["target"], KW.grant, eff.d["user"])
+def privileges_grantappend_handler(eff: dict):
+    return main_implementation(KW.append, eff["target"], KW.grant, eff["user"])
 
-def privileges_granthost_handler(eff: _Effect_Class):
-    return main_implementation(KW.host, eff.d["target"], KW.grant, eff.d["user"])
+def privileges_granthost_handler(eff: dict):
+    return main_implementation(KW.host, eff["target"], KW.grant, eff["user"])
 
-def privileges_grantdiscover_handler(eff: _Effect_Class):
-    return main_implementation(KW.discover, eff.d["target"], KW.grant, eff.d["user"])
+def privileges_grantdiscover_handler(eff: dict):
+    return main_implementation(KW.discover, eff["target"], KW.grant, eff["user"])
 
-def privileges_grantmodifyrights_handler(eff: _Effect_Class):
-    return main_implementation(KW.modify_rights, eff.d["target"], KW.grant, eff.d["user"])
+def privileges_grantmodifyrights_handler(eff: dict):
+    return main_implementation(KW.modify_rights, eff["target"], KW.grant, eff["user"])
 
-def privileges_revokeview_handler(eff: _Effect_Class):
-    return main_implementation(KW.view, eff.d["target"], KW.revoke, eff.d["user"])
+def privileges_revokeview_handler(eff: dict):
+    return main_implementation(KW.view, eff["target"], KW.revoke, eff["user"])
 
-def privileges_revokeappend_handler(eff: _Effect_Class):
-    return main_implementation(KW.append, eff.d["target"], KW.revoke, eff.d["user"])
+def privileges_revokeappend_handler(eff: dict):
+    return main_implementation(KW.append, eff["target"], KW.revoke, eff["user"])
 
-def privileges_revokehost_handler(eff: _Effect_Class):
-    return main_implementation(KW.host, eff.d["target"], KW.revoke, eff.d["user"])
+def privileges_revokehost_handler(eff: dict):
+    return main_implementation(KW.host, eff["target"], KW.revoke, eff["user"])
 
-def privileges_revokediscover_handler(eff: _Effect_Class):
-    return main_implementation(KW.discover, eff.d["target"], KW.revoke, eff.d["user"])
+def privileges_revokediscover_handler(eff: dict):
+    return main_implementation(KW.discover, eff["target"], KW.revoke, eff["user"])
 
-def privileges_revokemodifyrights_handler(eff: _Effect_Class):
-    return main_implementation(KW.modify_rights, eff.d["target"], KW.revoke, eff.d["user"])
\ No newline at end of file
+def privileges_revokemodifyrights_handler(eff: dict):
+    return main_implementation(KW.modify_rights, eff["target"], KW.revoke, eff["user"])
\ No newline at end of file
diff --git a/python/zef/core/fx/stream.py b/python/zef/core/fx/stream.py
index 1014bd18..45911e90 100644
--- a/python/zef/core/fx/stream.py
+++ b/python/zef/core/fx/stream.py
@@ -36,8 +36,8 @@ def stream_create_pushable_stream_handler(eff: Effect):
 
 
 def stream_push_handler(eff: Effect):   # -> Union[Nil, Error]
-    assert 'stream' in eff.d
-    s = eff.d['stream']
+    assert 'stream' in eff
+    s = eff['stream']
     z_stream = s.stream_ezefref
     assert isinstance(s, Awaitable)
     streams = _state['streams']
@@ -45,7 +45,7 @@ def stream_push_handler(eff: Effect):   # -> Union[Nil, Error]
         raise RuntimeError(f'in push effect handler: associated stream not found in fx._state["streams"]')
     
     # TODO: check that type of item agrees if stream is typed!!!!!!!!!!!!!!!!!!
-    streams[z_stream].on_next(eff.d['item'])
+    streams[z_stream].on_next(eff['item'])
     return None
 
 
diff --git a/python/zef/core/fx/sync.py b/python/zef/core/fx/sync.py
index 7d040b5a..f398603e 100644
--- a/python/zef/core/fx/sync.py
+++ b/python/zef/core/fx/sync.py
@@ -13,8 +13,13 @@
 # limitations under the License.
 
 from .fx_types import Effect
+from ..error import Error
 
 def graph_sync_handler(eff: Effect):
-    from ...pyzef.main import sync
-    sync(eff.d["graph"], eff.d["sync_state"])
-    return {}
+    try:
+        from ...pyzef.main import sync
+        sync(eff["graph"], eff["sync_state"])
+        return {}
+    except Exception as e:
+        return Error(f'executing FX.Graph.Sync for effect {eff}:\n{repr(e)}')
+
diff --git a/python/zef/core/fx/tag.py b/python/zef/core/fx/tag.py
index 40cf037c..3fdbb2bf 100644
--- a/python/zef/core/fx/tag.py
+++ b/python/zef/core/fx/tag.py
@@ -16,14 +16,23 @@
 from ..error import Error
 
 def graph_tag_handler(eff: Effect):
-    from ...pyzef.main import tag
-    tag(eff.d["graph"], eff.d["tag"], adding=eff.d["adding"], force=eff.d["force"])
-    return {}
+    try:
+        from ...pyzef.main import tag
+        tag(eff["graph"], eff["tag"], adding=eff["adding"], force=eff["force"])
+        return {}
+    except Exception as e:
+        return Error(f'executing FX.Graph.Tag for effect {eff}:\n{repr(e)}')
+
+
 
 
 def rae_tag_handler(eff: Effect):
-    from ...pyzef.main import tag
-    if not eff.d["adding"]:
-        return Error("Untagging a RAE is not supported (yet).")
-    tag(eff.d["rae"], eff.d["tag"], force_if_name_tags_other_rel_ent=eff.d["force"])
-    return {}
+    try:
+        from ...pyzef.main import tag
+        if not eff["adding"]:
+            return Error("Untagging a RAE is not supported (yet).")
+        tag(eff["rae"], eff["tag"], force_if_name_tags_other_rel_ent=eff["force"])
+        return {}
+    except Exception as e:
+        return Error(f'executing FX.RAE.Tag for effect {eff}:\n{repr(e)}')
+
diff --git a/python/zef/core/fx/zefhub.py b/python/zef/core/fx/zefhub.py
index 4c42eb31..a03fd10a 100644
--- a/python/zef/core/fx/zefhub.py
+++ b/python/zef/core/fx/zefhub.py
@@ -12,64 +12,72 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-from .fx_types import _Effect_Class
 from ...pyzef.internals import login, login_manual, logout
+from ..error import Error
+from .fx_types import Effect
 
-def zefhub_login_handler(eff: _Effect_Class):
-    jupyter_shell = False
+def zefhub_login_handler(eff: Effect):
     try:
-        import IPython
-        shell = IPython.get_ipython()
-        if shell is not None and shell.__class__.__name__ != 'TerminalInteractiveShell':
-            jupyter_shell = True
-    except:
-        pass
-    comments = """=================================
-You are now logged in to ZefHub. You can synchronize graphs which will enable them to be stored on
-ZefHub. Any ETs, RT, ENs and KWs you create will also be synchronized with ZefHub.
+        jupyter_shell = False
+        try:
+            import IPython
+            shell = IPython.get_ipython()
+            if shell is not None and shell.__class__.__name__ != 'TerminalInteractiveShell':
+                jupyter_shell = True
+        except:
+            pass
+        comments = """=================================
+    You are now logged in to ZefHub. You can synchronize graphs which will enable them to be stored on
+    ZefHub. Any ETs, RT, ENs and KWs you create will also be synchronized with ZefHub.
 
-Note: your credentials have been stored in the zef config directory.
-By default these will be used to automatically connect to ZefHub on import of the zef module.
-If you would like to change this behavior, please see the `config` zefop for more information.
-================================="""
+    Note: your credentials have been stored in the zef config directory.
+    By default these will be used to automatically connect to ZefHub on import of the zef module.
+    If you would like to change this behavior, please see the `config` zefop for more information.
+    ================================="""
 
-    if 'auth_key' in eff.d:
-        login_manual(eff.d['auth_key'])
-        comments = """=================================
-You are logged in as a guest user, which allows you to view public graphs but
-does not allow for synchronising new graphs with ZefHub.
+        if 'auth_key' in eff:
+            login_manual(eff['auth_key'])
+            comments = """=================================
+    You are logged in as a guest user, which allows you to view public graphs but
+    does not allow for synchronising new graphs with ZefHub.
+
+    Disclaimer: any ETs, RTs, ENs, and KWs that you query will be stored with ZefHub.
+    ================================="""
+        else:
+            if jupyter_shell:
+                print("=================================")
+                print("This command starts a local libzef login web server. If you are running inside")
+                print("of a jupyter notebook then this will start the auth prompt on the server where")
+                print("jupyter is running.")
+                print()
+                print("If you are using jupyter through a Google Colab server or a different JupyterHub")
+                print("server, then this will not be useful to you. In fact, we do not recommend you")
+                print("login with your credentials on a remote server.")
+                print()
+                print("You can instead login as a guest user using `login_as_guest | run`")
+                print()
+                print("Advanced users: If you are sure you would like to login with your credentials,")
+                print("then look for workarounds involving environment variables and manual login commands.")
+                print("=================================")
+            login()
 
-Disclaimer: any ETs, RTs, ENs, and KWs that you query will be stored with ZefHub.
-================================="""
-    else:
         if jupyter_shell:
-            print("=================================")
-            print("This command starts a local libzef login web server. If you are running inside")
-            print("of a jupyter notebook then this will start the auth prompt on the server where")
-            print("jupyter is running.")
-            print()
-            print("If you are using jupyter through a Google Colab server or a different JupyterHub")
-            print("server, then this will not be useful to you. In fact, we do not recommend you")
-            print("login with your credentials on a remote server.")
-            print()
-            print("You can instead login as a guest user using `login_as_guest | run`")
-            print()
-            print("Advanced users: If you are sure you would like to login with your credentials,")
-            print("then look for workarounds involving environment variables and manual login commands.")
-            print("=================================")
-        login()
+            print(comments)
+            # TODO: Report user account here
+            return {"success": True}
+        else:
+            # TODO: Report user account here
+            return {"success": True,
+                    "comments": comments}
+    except Exception as e:
+        return Error(f'executing FX.ZefHub.Login for effect {eff}:\n{repr(e)}')
 
-    if jupyter_shell:
-        print(comments)
-        # TODO: Report user account here
-        return {"success": True}
-    else:
-        # TODO: Report user account here
-        return {"success": True,
-                "comments": comments}
 
-def zefhub_logout_handler(eff: _Effect_Class):
-    logout()
-    print("Warning, after logging out all graphs will have been unloaded. Any accesses of a ZefRef")
-    print("referencing these old graphs will cause a segmentation fault!")
-    return {"success": True}
\ No newline at end of file
+def zefhub_logout_handler(eff: Effect):
+    try:
+        logout()
+        print("Warning, after logging out all graphs will have been unloaded. Any accesses of a ZefRef")
+        print("referencing these old graphs will cause a segmentation fault!")
+        return {"success": True}
+    except Exception as e:
+        return Error(f'executing FX.ZefHub.Login for effect {eff}:\n{repr(e)}')
diff --git a/python/zef/core/graph_delta.py b/python/zef/core/graph_delta.py
index e76e3a0e..46fa6382 100644
--- a/python/zef/core/graph_delta.py
+++ b/python/zef/core/graph_delta.py
@@ -15,7 +15,7 @@
 __all__ = [
     "GraphDelta",
 ]
-from typing import Any, Tuple, Callable
+from typing import Tuple, Callable
 from functools import partial as P
 
 from .. import pyzef
@@ -26,6 +26,8 @@
 from .op_structs import ZefOp, LazyValue
 from .graph_slice import GraphSlice
 from .abstract_raes import Entity, Relation, AtomicEntity, TXNode, Root
+from .logger import log
+from .VT import Is, Any
 
 from abc import ABC
 class ListOrTuple(ABC):
@@ -35,6 +37,8 @@ class ListOrTuple(ABC):
 
 
 
+import os
+gd_timing = "ZEFDB_GRAPH_DELTA_TIMING" in os.environ
 
 ##############################
 # * Description
@@ -45,70 +49,23 @@ class ListOrTuple(ABC):
 # - commands: dictionaries in the constructed GraphDelta which match directly to low-level changes.
 # - expressions: high-level things which match to user expectations. There are "complex" expressions and "primitive" expressions.
 #
-# The general flow to take a bunch of complex exprs, turn them into primitive
-# exprs (may increase the number of exprs) and then turn these into commands.
-#
-# GraphDelta constructor is in three stages:
-# First stage: verify input and translate to "simpler" or "fewer" base exprs.
-# Second stage: convert exprs to commands and order them
-# Orders them by iterating on to-do commands, sorts into bucket of can-be-done-now and to-do-later, applies can-be-done-now, iterate
-#
-# Logic flow of GraphDelta constructor is:
-# - handles nested GraphDeltas (including reassigning unique temporary ids)
-# - calls verify_internal_id on each expr (this builds a ID dict for the next step)
-# - calls verify_input_el on each expr (this validates that all Z[...] are in the ID dict, as well as the exprs have the expected structure)
-# - calls verify_relation_source_target (checks abstract Relations have all source/targets)
-# - TODO: call verify no TX or root nodes
-# - iteratively calls iteration_step (make_iteration_step)
-# - calls verify_and_compact_commands - does ordering
-#
-# iteration_step is a dispatch on expr type to call functions with _cmds_for prefix (e.g. _cmds_for_instantiable)
-# - takes in exprs-to-do ("user-expressions"), finished-commands ("commands") and seen-ids ("ids_present")
-# - returns new exprs, new commands and new ids
-# - eventually exprs list should "run out" and everything is now a command
-# - another analogy: exprs is a "work queue", cmds/ids is an "output list", iteration_step takes from work queue, puts results into output and maybe more jobs into the work queue.
-#
-#
-# Shorthand syntax:
-#
-# e.g. (a, (b,c,d)) = [ET.Entity, (z, RT.Something, 5)] | g | run
-#
-# If the user uses the shorthand syntax then the logic first passes through
-# `encode`. The function encode only does two additional things to the
-# GraphDelta constructor - it assigns IDs where necessary to be able to return
-# appropriate results, and prevents a few unusual things (e.g. tagging a
-# ZefRef). Most of this function is dedicated to handling relations to be able
-# to unpack them into the right structures. Other nodes are left "as is" except
-# for obtaining iids to unpack them.
-#
-#
-#
-# realise_single_node:
-# Used by encode for every top level item and used by GraphDelta constructor and
-# encode for pieces of relations. This function exists to both assign an ID and
-# convert a complex expression to a primitive one. Only works on "single
-# objects", e.g. entity, value, ZefRef, and not relations or tags.
-#
-# e.g. realise_single_node(ET.Entity) = ("tmp_id_1", ET.Entity["tmp_id_1"])
-#
-# 
-# Performing GraphDelta transaction:
-# - works through list of commands, applying then in a low-level transaction onto the graph
-# - commands must be properly ordered (done in constructor) before this function is called
-# - each command fills in ID dictionary to be returned as receipt.
-# - ID dictionary is called "d_raes"
-#
 
-# types of commands:
-# - instantiate
-# - assign_value
-# - merge
-# - terminate
-# - tag
-#
-# Commands should use keywords of "internal_id" or "origin_rae" to fill in ID dictionary.
+# TODO: REDO THE DESCRIPTION OF THIS FILE
+
 
 
+# WARNING!!!
+# WARNING!!!
+# WARNING!!!
+#
+# Much of the logic flow in this file is written in an immutable functional
+# style. However, for speed many of these functions have been changed to a
+# mutating version. This can lead to confusion when debugging.
+#
+# WARNING!!!
+# WARNING!!!
+# WARNING!!!
+
 
 ########################################################
 # * Entrypoint from user code
@@ -133,7 +90,7 @@ def insert_id_maybe(cmd: dict):
 
         commands_with_ids = [insert_id_maybe(c) for  c in commands]
         return Effect({
-                "type": FX.TX.Transact,
+                "type": FX.Graph.Transact,
                 "target_graph": g,
                 "commands": commands_with_ids,
                 "unpacking_template": unpacking_template,
@@ -150,8 +107,9 @@ def insert_id_maybe(cmd: dict):
 ##############################
 # * GraphDelta construction
 #----------------------------
-def construct_commands(elements) -> list:
-    generate_id = make_generate_id()    # keeps an internal counter        
+def construct_commands(elements, generate_id=None) -> list:
+    if generate_id is None:
+        generate_id = make_generate_id()    # keeps an internal counter        
 
     # First extract any nested GraphDeltas commands out
     # Note: using native python filtering as the evaluation engine is too slow
@@ -184,9 +142,15 @@ def update_ids(cmd, mapping_dict):
     # elements = elements | map[handle_dict | first] | concat | collect
     # Obtain all user IDs
     id_definitions = elements | map[obtain_ids] | reduce[merge_no_overwrite][{}] | collect
+    if gd_timing:
+        log.debug("Got id_definitions")
     # Check that nothing needs a user ID that wasn't provided
     elements | for_each[verify_input_el[id_definitions]]
+    if gd_timing:
+        log.debug("Verified input_el")
     elements | for_each[verify_relation_source_target[id_definitions]]
+    if gd_timing:
+        log.debug("Verified relation_source_target")
 
     state_initial = {
         'user_expressions': tuple(elements),
@@ -200,15 +164,16 @@ def debug_output(x):
             nonlocal num_done, next_print
             num_done += 1
             if now() > next_print:
-                print(f"Up to iteration {num_done} - {len(x['user_expressions'])}, {len(x['commands'])}", now())
+                log.debug("Construct: Up to", num_done=num_done, num_expr=len(x['user_expressions']), num_cmd=len(x['commands']))
                 next_print = now() + 5*seconds
         return debug_output
         
     # iterate until all user expressions and generated expressions have become commands
     state_final = (state_initial
                    | iterate[iteration_step[generate_id]]
-                   # | map[tap[make_debug_output()]]
+                   | (map[tap[make_debug_output()]] if gd_timing else identity)
                    | take_until[lambda s: len(s['user_expressions'])==0]
+                   # | map[tap[print]]
                    | last
                    | collect
                    )
@@ -280,13 +245,17 @@ def verify_input_el(x, id_definitions, allow_rt=False, allow_scalar=False):
 
     if isinstance(x, LazyValue):
         # This is checking for an (x <= value) format
-        if is_a(x.el_ops, assign_value):
+        if is_a(x.el_ops, assign):
             return
         elif is_a(x.el_ops, terminate):
             return
         elif is_a(x.el_ops, tag):
             return
-        raise Exception(f"A LazyValue must have come from (x | assign_value), (x | terminate) or (x | tag) only. Got {x}")
+        elif is_a(x.el_ops, fill_or_attach):
+            return
+        elif is_a(x.el_ops, set_field):
+            return
+        raise Exception(f"A LazyValue must have come from (x | assign), (x | terminate), (x | fill_or_attach) or (x | tag) only. Got {x}")
 
     # Note: just is_a(x, Z) will also mean ZefRefs will be hit
     elif is_a(x, ZefOp) and is_a(x, Z):
@@ -305,7 +274,10 @@ def verify_input_el(x, id_definitions, allow_rt=False, allow_scalar=False):
 
         for k,v in sub_d.items():
             verify_input_el(k, id_definitions, True, False)
-            verify_input_el(v, id_definitions, False, True)
+            if isinstance(v, list):
+                [verify_input_el(e, id_definitions, False, True) for e in v]
+            else:
+                verify_input_el(v, id_definitions, False, True)
 
         return
 
@@ -335,7 +307,7 @@ def verify_input_el(x, id_definitions, allow_rt=False, allow_scalar=False):
 
     elif isinstance(x, ZefOp):
         if len(x) > 1:
-            # This could be something like Z["asdf"] | assign_value[5]
+            # This could be something like Z["asdf"] | assign[5]
             if LazyValue(x) | first | is_a[Z] | collect:
                 return
         if length(LazyValue(x) | absorbed) != 1:
@@ -426,14 +398,18 @@ def dispatch_cmds_for(expr, gen_id):
 
     # If we have a lazy value the second time around, then this must be an actual action to perform.
     if isinstance(expr, LazyValue):
-        if is_a(expr.el_ops, assign_value):
-            return cmds_for_lv_assign_value(expr, gen_id)
+        if is_a(expr.el_ops, assign):
+            return cmds_for_lv_assign(expr, gen_id)
         elif is_a(expr.el_ops, terminate):
             return cmds_for_lv_terminate(expr)
+        elif is_a(expr.el_ops, set_field):
+            return cmds_for_lv_set_field(expr, gen_id)
+        elif is_a(expr.el_ops, fill_or_attach):
+            return cmds_for_complex_expr(expr, gen_id)
         elif is_a(expr.el_ops, tag):
             return cmds_for_lv_tag(expr, gen_id)
         else:
-            raise Exception("LazyValue obtained which is not an assign_value or terminate")
+            raise Exception("LazyValue obtained which is not an assign or terminate")
 
     elif isinstance(expr, ZefOp):
         # If we have a chain beginning with a Z, convert to LazyValue
@@ -478,7 +454,7 @@ def cmds_for_complex_expr(x, gen_id):
     return exprs, []
 
 def cmds_for_initial_Z(expr):
-    assert LazyValue(expr) | first | is_a[Z] | collect
+    assert LazyValue(expr) | first | And[is_a[ZefOp]][is_a[Z]] | collect
 
     expr = LazyValue(LazyValue(expr) | first | collect) | (LazyValue(expr) | skip[1] | to_pipeline | collect)
     return (expr,), ()
@@ -510,7 +486,7 @@ def cmds_for_mergable(x):
 
         elif BT(x) == BT.ATOMIC_ENTITY_NODE:
             assign_val_cmds = [] if isinstance(x, EZefRef) else [{
-                'cmd': 'assign_value', 
+                'cmd': 'assign', 
                 'value': x | value | collect,
                 'internal_id': uid(origin),
                 'explicit': False,
@@ -556,17 +532,17 @@ def cmds_for_lv_tag(x, gen_id):
     return exprs, [cmd]
     
 
-def cmds_for_lv_assign_value(x, gen_id: Callable):
+def cmds_for_lv_assign(x, gen_id: Callable):
     assert isinstance(x, LazyValue)
 
     assert len(x | peel | collect) == 2
     target,op = x | peel | collect
 
-    assert is_a(op, assign_value)
+    assert is_a(op, assign)
     val = LazyValue(op) | absorbed | single | collect
     iid,exprs = realise_single_node(target, gen_id)
 
-    return exprs, [{'cmd': 'assign_value', 'value': val, 'explicit': True, "internal_id": iid}]
+    return exprs, [{'cmd': 'assign', 'value': val, 'explicit': True, "internal_id": iid}]
 
 def cmds_for_lv_terminate(x):
     assert isinstance(x, LazyValue)
@@ -585,6 +561,37 @@ def cmds_for_lv_terminate(x):
 
     return (), [cmd]
 
+def cmds_for_lv_set_field(x, gen_id):
+    assert isinstance(x, LazyValue)
+
+    assert len(x | peel | collect) == 2
+    source,op = x | peel | collect
+
+    # assert is_a(source, ZefOp) and is_a(source, Z), f"fill_or_attach reached cmd creation with an incorrect input type: {source}"
+    assert is_a(op, set_field)
+    rt,assignment = LazyValue(op) | absorbed | collect
+    assert type(rt) == RelationType
+
+    iid,exprs = realise_single_node(source, gen_id)
+
+    cmd = {
+        'cmd': 'set_field', 
+        'source_id': iid,
+        'rt': LazyValue(rt) | without_absorbed | collect,
+    }
+
+    if type(assignment) in scalar_types:
+        cmd['value'] = assignment 
+    else:
+        target_iid,target_exprs = realise_single_node(assignment, gen_id)
+        cmd['target_id'] = target_iid
+        exprs.extend(target_exprs)
+
+    if LazyValue(rt) | absorbed | collect != ():
+        cmd['internal_id'] = LazyValue(rt) | absorbed | single | collect
+
+    return exprs, [cmd]
+
 def cmds_for_delegate(x):                            
     internal_ids = []
     a_id = get_absorbed_id(x)
@@ -735,18 +742,26 @@ def realise_single_node(x, gen_id):
     if isinstance(x, LazyValue):
         x = collect(x)
 
-    # Now this is a check for whether we are an assign_value
+    # Now this is a check for whether we are an assign
     if isinstance(x, LazyValue):
         target,op = x | peel | collect
-        if is_a(op, assign_value):
-            iid,exprs = realise_single_node(target, gen_id)
-            exprs = exprs + [LazyValue(Z[iid]) | op]
-        elif is_a(op, terminate):
-            iid = origin_uid(target)
-            exprs = [x]
-        elif is_a(op, tag):
+        if is_a(op, terminate) or is_a(op, tag):
             iid = origin_uid(target)
             exprs = [x]
+        elif is_a(op, assign):
+            iid,exprs = realise_single_node(target, gen_id)
+            exprs = exprs + [LazyValue(Z[iid]) | op]
+        elif is_a(op, fill_or_attach):
+            # fill_or_attach behaviour is now basically set_field except when the target is not a value
+            iid,exprs = realise_single_node(target, gen_id)
+            rt,assignment = LazyValue(op) | absorbed | collect
+            if type(assignment) in scalar_types:
+                exprs = exprs + [LazyValue(Z[iid]) | set_field[rt][assignment]]
+            else:
+                exprs = exprs + [(Z[iid], rt, assignment)]
+        elif is_a(op, set_field):
+            iid,exprs = realise_single_node(target, gen_id)
+            exprs = exprs + [LazyValue(Z[iid]) | op]
         else:
             raise Exception(f"Don't understand LazyValue type: {op}")
     elif isinstance(x, EntityType) or isinstance(x, AtomicEntityType):
@@ -771,7 +786,7 @@ def realise_single_node(x, gen_id):
     elif type(x) in scalar_types:
         iid = gen_id()
         aet = map_scalar_to_aet_type[type(x)](x)
-        exprs = [aet[iid], LazyValue(Z[iid]) | assign_value[x]]
+        exprs = [aet[iid], LazyValue(Z[iid]) | assign[x]]
     elif isinstance(x, ZefOp):
         assert len(x) == 1
         params = LazyValue(x) | peel | first | second
@@ -790,9 +805,13 @@ def realise_single_node(x, gen_id):
 
         exprs = entity_exprs
         for k,v in sub_d.items():
-            target_iid, target_exprs = realise_single_node(v, gen_id)
-            exprs.extend(target_exprs)
-            exprs.append((Z[iid], k, Z[target_iid]))
+            if isinstance(v, list):
+                # for e in v:
+                #     assert type(e) not in scalar_types, "Can't have multiple scalars attached in a dictionary"
+                #     exprs.append((Z[iid], k, e))
+                raise Exception("Not allowed to use lists inside of a dictionary syntax anymore")
+            else:
+                exprs.append(Z[iid] | set_field[k][v])
     else:
         raise TypeError(f'in GraphDelta encode step: for type(x)={type(x)}')
 
@@ -806,26 +825,26 @@ def realise_single_node(x, gen_id):
 
 def verify_and_compact_commands(cmds: tuple):                
     aes_with_explicit_assigns = (cmds
-                                 | filter[lambda d: d["cmd"] == "assign_value"]
+                                 | filter[lambda d: d["cmd"] == "assign"]
                                  | filter[lambda d: d["explicit"]]
                                  | map[lambda d: d["internal_id"]]
                                  | collect
                                  )
     def is_unnecessary_automatic_merge_assign(d):
-        return (d["cmd"] == "assign_value"
+        return (d["cmd"] == "assign"
                 and not d["explicit"]
                 and d["internal_id"] in aes_with_explicit_assigns)
 
     # make sure if multiple assignment commands exist for the same AE, that their values agree
     cmds = (cmds | group_by[get['cmd']]
-            | map[match_apply[
-                        (first | equals["assign_value"],
-                         second | filter[Not[is_unnecessary_automatic_merge_assign]]
-                         | validate_and_compress_unique_assignment),
-                        (first | equals["merge"], second | combine_internal_ids_for_merges),
-                        (first | equals["terminate"], second | combine_terminates),
-                        # Just pack things back in for other cmd types
-                        (always[True], second)
+            | map[match[
+                        (Is[first | equals["assign"]],   second | filter[Not[is_unnecessary_automatic_merge_assign]] | validate_and_compress_unique_assignment),
+                # TODO: Validate fill_or_attach assignments - but note that
+                # these could also conflict with regular assignments but it's
+                # hard to tell that without playing out the assignments first.
+                        (Is[first | equals["merge"]],          second | combine_internal_ids_for_merges),
+                        (Is[first | equals["terminate"]],      second | combine_terminates), 
+                        (Any, second),                                 # Just pack things back in for other cmd types
             ]]
             | concat
             | collect)
@@ -842,7 +861,7 @@ def debug_output(x):
             nonlocal num_done, next_print
             num_done += 1
             if now() > next_print:
-                print(f"Up to compacting {num_done} - {len(x['state']['input'])}, {len(x['state']['output'])}", now())
+                log.debug("Compacting:", num_done=num_done, num_input=len(x['state']['input']), num_output=len(x['state']['output']))
                 next_print = now() + 5*seconds
         return debug_output
 
@@ -854,9 +873,9 @@ def debug_output(x):
     state_final = (
         {"state": state_initial, "num_changed": -1}
         | iterate[resolve_dag_ordering_step] 
-        # | tap[make_debug_output()]
+        | (tap[make_debug_output()] if gd_timing else identity)
         | take_until[lambda s: s["num_changed"] == 0]
-        # | tap[lambda x: print("After take_until")]
+        # | map[tap[print]]
         | last
         | get["state"]
         | collect
@@ -927,7 +946,8 @@ def command_ordering_by_type(d_raes: dict) -> int:
         if isinstance(d_raes['rae_type'], AtomicEntityType): return 2
         if isinstance(d_raes['rae_type'], RelationType): return 3
         return 4                                            # there may be {'cmd': 'instantiate', 'rae_type': AET.Bool}
-    if d_raes['cmd'] == 'assign_value': return 5
+    if d_raes['cmd'] == 'assign': return 5
+    if d_raes['cmd'] == 'set_field': return 5.5
     if d_raes['cmd'] == 'terminate' and isinstance(d_raes['origin_rae'], Relation): return 6
     if d_raes['cmd'] == 'terminate' and isinstance(d_raes['origin_rae'], Entity): return 7
     if d_raes['cmd'] == 'terminate' and isinstance(d_raes['origin_rae'], AtomicEntity): return 8
@@ -965,15 +985,35 @@ def resolve_dag_ordering_step(arg: dict)->dict:
     # arg is "state" + "num_changed"
     state = arg["state"]
     ids = state['known_ids']        
+    input = state['input']
     
     def can_be_executed(cmd):        
-        if cmd['cmd'] == 'instantiate' and isinstance(cmd['rae_type'], RelationType) and (cmd['source'] not in ids or cmd['target'] not in ids):
-            return False
-        if cmd['cmd'] == 'merge' and isinstance(cmd['origin_rae'], Relation) and (cmd['origin_rae'].d["uids"][0] not in ids or cmd['origin_rae'].d["uids"][2] not in ids):
-            return False
-        if cmd['cmd'] == 'terminate' and any(get_id(cmd) in get_ids(other) for other in state['input'] if other['cmd'] != 'terminate'):
-            return False
-        return True        
+        if cmd['cmd'] == 'instantiate':
+            # If we are creating an RT, wait until both source/target exist
+            return not isinstance(cmd['rae_type'], RelationType) or (cmd['source'] in ids and cmd['target'] in ids)
+        if cmd['cmd'] == 'merge':
+            # If the merge is of a relation, we need both source and target to exist already
+            return not isinstance(cmd['origin_rae'], Relation) or (cmd['origin_rae'].d["uids"][0] not in ids or cmd['origin_rae'].d["uids"][2] not in ids)
+        if cmd['cmd'] == 'terminate':
+            # Don't terminate if there an upcoming operation that will create this item
+            return all(get_id(cmd) not in get_ids(other) for other in input if other['cmd'] != 'terminate')
+        if cmd['cmd'] == 'assign':
+            # The object to be assigned needs to exist.
+            return get_id(cmd) in ids
+        if cmd['cmd'] == 'set_field':
+            # This is where things get tricky - the behaviour of set_field
+            # can change if there is another command that creates a relation of
+            # the same type.
+            #
+            # TODO: this properly! I can see issues with assignments to the same
+            # object via different IDs causing massive headaches here. Need to
+            # consider aliasing and also consider multiple set_field commands.
+            #
+            # For now, just make sure the source is alive and run with that.
+            return cmd['source_id'] in ids
+        if cmd['cmd'] == 'tag':
+            return get_id(cmd) in ids
+        raise Exception(f"Don't know how to decide DAG ordering for command {cmd['cmd']}")
     (_,can), (_,cannot) = state['input'] | group_by[can_be_executed][[True, False]] | collect
     return {
         "state": {
@@ -1072,7 +1112,7 @@ def step(x, allow_scalar):
         return iid
 
     step_res = step(xx, False)
-    return step_res, construct_commands(gd_exprs)
+    return step_res, construct_commands(gd_exprs, gen_id)
 
 
 def unpack_receipt(unpacking_template, receipt: dict):
@@ -1099,7 +1139,13 @@ def perform_transaction_commands(commands: list, g: Graph):
             # TODO: Have to change the behavior of Transaction(g) later I suspect
             frame_now = GraphSlice(tx_now)
             d_raes['tx'] = tx_now
+
+            next_print = now()+5*seconds
             for i,cmd in enumerate(commands):
+                if gd_timing:
+                    if now() > next_print:
+                        log.debug("Perform", i=i, total=len(commands))
+                        next_print = now() + 5*seconds
 
                 zz = None
                 
@@ -1110,7 +1156,7 @@ def perform_transaction_commands(commands: list, g: Graph):
                 elif cmd['cmd'] == 'instantiate' and type(cmd['rae_type']) in {RelationType}:
                     zz = instantiate(to_ezefref(d_raes[cmd['source']]), cmd['rae_type'], to_ezefref(d_raes[cmd['target']]), g) | in_frame[frame_now] | collect
                 
-                elif cmd['cmd'] == 'assign_value':
+                elif cmd['cmd'] == 'assign':
                     this_id = cmd['internal_id']
                     zz = d_raes.get(this_id, None)
                     if zz is None:
@@ -1121,6 +1167,49 @@ def perform_transaction_commands(commands: list, g: Graph):
                             # print("Assigning value of ", cmd['value'], "to a", AET(z))
                             internals.assign_value_imp(zz, cmd['value'])
                     zz = now(zz)
+
+                elif cmd['cmd'] == 'set_field':
+                    z_source = d_raes.get(cmd['source_id'], None)
+                    if z_source is None:
+                        raise KeyError(f"set_field called with unknown source {cmd['source_id']}")
+
+                    if 'value' in cmd:
+                        # AET path
+                        aet = map_scalar_to_aet_type[type(cmd['value'])](cmd['value'])
+                    else:
+                        # Entity path
+                        z_target = d_raes.get(cmd['target_id'], None)
+                        if z_target is None:
+                            raise KeyError("set_field called with entity that is not known {cmd['target_id']}")
+
+                    opts = z_source | out_rels[cmd['rt']] | collect
+                    if len(opts) == 2:
+                        raise Exception(f"Can't set_field to {z_source} because it has two or more relations of kind {rt}")
+                    elif len(opts) == 1:
+                        zz = single(opts)
+                        if 'value' in cmd:
+                            # AE path - overwrite value
+                            ae = target(zz)
+                            if aet != rae_type(ae):
+                                raise Exception(f"Can't fill or attach {ae} because it is the wrong type for value {cmd['value']}")
+                            if value(ae) != cmd['value']:
+                                internals.assign_value_imp(ae, cmd['value'])
+                        else:
+                            # Entity path - replace the relation
+                            from ..pyzef import zefops as pyzefops
+                            pyzefops.terminate(zz)
+                            zz = instantiate(z_source, cmd['rt'], z_target, g)
+                    else:
+                        if 'value' in cmd:
+                            # AE path
+                            ae = instantiate(aet, g)
+                            internals.assign_value_imp(ae, cmd['value'])
+                            zz = instantiate(z_source, cmd['rt'], ae, g)
+                        else:
+                            # Entity path
+                            zz = instantiate(z_source, cmd['rt'], z_target, g)
+                    # This zz is the relation that connects the source/target
+                    zz = now(zz)
                     
                 elif cmd['cmd'] == 'merge':
                     # It is either an instance (with an 'origin_rae_uid' specified)
@@ -1203,6 +1292,25 @@ def perform_transaction_commands(commands: list, g: Graph):
                 for this_id in get_ids(cmd):
                     d_raes[this_id] = zz
 
+        # There is a weird edge case here - if a node is asked to be merged
+        # in, but already existed on the graph AND there were no changes,
+        # causing the transaction to be rolled-back, then the reference
+        # frame of the ZefRefs will be wrong.
+
+        # We check this by seeing if the index of the tx zefref is invalid.
+        if index(d_raes['tx']) > g.graph_data.read_head:
+            # Signal this by setting the tx to none (i.e. there was no tx)
+            d_raes['tx'] = None
+
+            # Update all ZefRefs to be in the latest frame instead of the previously created tx.
+            reset_frame = in_frame[now(g)][allow_tombstone]
+            for key,val in d_raes.items():
+                if key == 'tx': continue
+
+                # A terminated non-existent entity can return None
+                if val is not None:
+                    d_raes[key] = reset_frame(val)
+
     except Exception as exc:        
         raise RuntimeError(f"Error executing graph delta transaction exc={exc}") from exc
         
@@ -1344,15 +1452,35 @@ def get_absorbed_id(obj):
     return obj | absorbed | single_or[None] | collect
 
 
+def equal_identity(a, b):
+    if type(a) != type(b):
+        return False
+
+    # Things that don't produce the same objectt even though their python objects
+    # are the same
+    if type(a) in [EntityType, RelationType, AtomicEntityType, ZefEnumValue]:
+        return False
+
+    return a == b
+
+# @func
+# def merge_no_overwrite(a,b):
+#     d = {**a}
+
+#     for k,v in b.items():
+#         if k in d and not equal_identity(d[k], v):
+#             raise Exception(f"The internal id '{k}' refers to multiple objects, including '{d[k]}' and '{v}'. This is ambiguous and not allowed.")
+#         d[k] = v
+#     return d
+
 @func
 def merge_no_overwrite(a,b):
-    d = {**a}
-
+    # This version is mutating because otherwise it's too slow
     for k,v in b.items():
-        if k in d and d[k] != v:
-            raise Exception("The internal id '{k}' refers to multiple objects, including '{d[k]}' and '{v}'. This is ambiguous and not allowed.")
-        d[k] = v
-    return d
+        if k in a and not equal_identity(a[k], v):
+            raise Exception(f"The internal id '{k}' refers to multiple objects, including '{a[k]!r}' and '{v!r}'. This is ambiguous and not allowed.")
+        a[k] = v
+    return a
 
 
 
diff --git a/python/zef/core/graph_slice.py b/python/zef/core/graph_slice.py
index cd47d173..00dd263b 100644
--- a/python/zef/core/graph_slice.py
+++ b/python/zef/core/graph_slice.py
@@ -60,7 +60,7 @@ def __init__(self, *args):
         
         
     def __repr__(self):
-        return f"GraphSlice(graph_uid='{str(uid(Graph(self.tx)))}', time_slice={int(time_slice(self))})"
+        return f"GraphSlice(graph_uid='{str(uid(Graph(self.tx)))}', index={graph_slice_index(self)})"
 
     def __eq__(self, other):        
         return (
@@ -73,7 +73,7 @@ def __int__(self):
         """
         Returns the time slice as an integer
         """
-        return int(self | time_slice | collect)
+        return self | graph_slice_index | collect
 
     def __hash__(self):
         return hash(self.tx)
diff --git a/python/zef/core/internals/__init__.py b/python/zef/core/internals/__init__.py
index 05e22ab8..56b9fed1 100644
--- a/python/zef/core/internals/__init__.py
+++ b/python/zef/core/internals/__init__.py
@@ -104,7 +104,11 @@
     wait_for_auth,
 )
 
-from ...pyzef.main import ZefRef
+from ...pyzef.verification import (
+    verify_graph
+)
+
+from ...pyzef.main import ZefRef, zwitch
 
 # ----------------------------- monkey patch to allow dispatching on enums ---------------------------------------
 # For some reason we need to pass it through a lambda fct to get the bindings to work.
@@ -118,6 +122,8 @@
 from .rel_ent_classes import ET, RT, EN, AET
 from .merges import register_merge_handler
 register_merge_handler()
+from .schema import register_schema_validator
+register_schema_validator()
 
 BT = BlobTypeStruct()
 
@@ -131,7 +137,7 @@ def safe_current_task():
 global_transaction_task = {}
 from contextlib import contextmanager
 @contextmanager
-def Transaction(g, wait=None):
+def Transaction(g, wait=None, rollback_empty=None, check_schema=None):
     global global_transaction_task
     from ...pyzef.zefops import uid
 
@@ -150,10 +156,14 @@ def Transaction(g, wait=None):
             del global_transaction_task[uid(g)]
         else:
             global_transaction_task[uid(g)] = prev_val
+
         if wait is None:
-            FinishTransaction(g)
-        else:
-            FinishTransaction(g, wait)
+            wait = zwitch.default_wait_for_tx_finish()
+        if rollback_empty is None:
+            rollback_empty = zwitch.default_rollback_empty_tx()
+        if check_schema is None:
+            check_schema = True
+        FinishTransaction(g, wait, rollback_empty, check_schema)
 
 
 
diff --git a/python/zef/core/internals/schema.py b/python/zef/core/internals/schema.py
new file mode 100644
index 00000000..9b7d43a5
--- /dev/null
+++ b/python/zef/core/internals/schema.py
@@ -0,0 +1,32 @@
+# Copyright 2022 Synchronous Technologies Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+def c_schema_validator(ctx):
+    """Don't call this explicitly. Only for the zefdb core."""
+
+    # We can move this to another section later on
+    from .. import Graph
+    from .. import _ops as zo
+    from ...pyzef.internals import AbortTransaction, RT
+
+    gs = ctx | zo.to_graph_slice | zo.collect
+    for schema in gs | zo.root | zo.Outs[RT.ZEF_Schema]:
+        lt = schema | zo.value | zo.collect
+        if {"graph_slice": gs} | zo.Not[zo.is_a[lt]] | zo.collect:
+            raise Exception(f"{schema} failed to validate graph slice.")
+
+def register_schema_validator():
+    from ...pyzef import internals
+    internals.register_schema_validator(c_schema_validator)
diff --git a/python/zef/core/logger.py b/python/zef/core/logger.py
index 7b8d5258..267ec28e 100644
--- a/python/zef/core/logger.py
+++ b/python/zef/core/logger.py
@@ -24,7 +24,7 @@
         # structlog.stdlib.PositionalArgumentsFormatter(),
         structlog.processors.TimeStamper(fmt="iso"),
         # The following only does anything if exc_info is passed as a keyword
-        structlog.processors.format_exc_info,
+        # structlog.processors.format_exc_info,
         structlog.processors.UnicodeDecoder(),
         structlog.dev.ConsoleRenderer(colors=True),
     ]
diff --git a/python/zef/core/op_implementations/dispatch_dictionary.py b/python/zef/core/op_implementations/dispatch_dictionary.py
index b92c9713..37909ca7 100644
--- a/python/zef/core/op_implementations/dispatch_dictionary.py
+++ b/python/zef/core/op_implementations/dispatch_dictionary.py
@@ -16,7 +16,6 @@
 from .implementation_typing_functions import * 
 from .yo import yo_implementation, yo_type_info
 from .graphviz import graphviz_imp, graphviz_tp
-from .func import call_implementation, call_type_info
 from .func import unpack_implementation, unpack_type_info
 
 _op_to_functions = {
@@ -26,6 +25,7 @@
         RT.Reduce:         (reduce_implementation, reduce_type_info),
         RT.Scan:           (scan_implementation, scan_type_info),
         RT.GroupBy:        (group_by_implementation, group_by_type_info),
+        RT.Group:          (group_imp, None),
         RT.Transpose:      (transpose_imp, transpose_tp),
         RT.Frequencies:    (frequencies_imp, frequencies_tp),
         RT.Iterate:        (iterate_implementation, iterate_type_info),
@@ -36,7 +36,6 @@
         RT.TakeWhilePair:  (take_while_pair_imp, take_while_pair_tp),
         RT.TakeUntil:      (take_until_imp, take_until_tp),
         RT.SkipWhile:      (skip_while_imp, skip_while_tp),
-        RT.Drop :          (drop_imp, drop_tp),
         RT.Skip :          (skip_imp, skip_tp),
         RT.Nth:            (nth_implementation, nth_type_info),
         RT.Filter:         (filter_implementation, filter_type_info),
@@ -58,6 +57,7 @@
         RT.Sort:            (sort_implementation, sort_type_info),
         RT.Now:             (now_implementation, now_type_info),
         RT.Events:          (events_imp, events_tp),
+        RT.PrecedingEvents: (preceding_events_imp, None),
         RT.ToDelegate:      (to_delegate_implementation, to_delegate_type_info),
         RT.DelegateOf:      (delegate_of_implementation, delegate_of_type_info),
         
@@ -75,20 +75,15 @@
         RT.OutOld:             (OutOld_implementation, None),
         RT.OutOutOld:          (OutOutOld_implementation, None),
         
-        RT.InsOld:          (ins_implementation_old, ins_type_info),
-        RT.OutsOld:         (outs_implementation_old, outs_type_info),
-        RT.InsAndOutsOld:   (ins_and_outs_implementation_old, ins_and_outs_type_info),
         RT.Source:          (source_implementation, source_type_info),
         RT.Target:          (target_implementation, target_type_info),
         RT.Value:           (value_implementation, None),
         RT.Time:            (time_implementation, time_type_info),
         RT.TimeSlice:       (time_slice_implementation, time_slice_type_info),
-        RT.Tx:              (tx_imp, tx_tp),
         RT.NextTX:          (next_tx_imp, next_tx_tp),
         RT.PreviousTX:      (previous_tx_imp, previous_tx_tp),
         RT.InstantiationTx: (instantiation_tx_implementation, instantiation_tx_type_info),                              # TODO: retire
         RT.TerminationTx:   (termination_tx_implementation, termination_tx_type_info),                            # TODO: retire
-        RT.Instances:       (instances_implementation, instances_type_info),
         RT.Uid:             (uid_implementation, uid_type_info),
         RT.Frame:           (frame_imp, frame_tp),
         RT.DiscardFrame:    (discard_frame_imp, None),
@@ -96,6 +91,7 @@
         RT.OriginUid:       (origin_uid_imp, origin_uid_tp),
         RT.OriginRAE:       (origin_rae_imp, origin_rae_tp),
         RT.ExistsAt:        (exists_at_implementation, exists_at_type_info),
+        RT.AwareOf:         (aware_of_implementation, None),
         RT.IsZefRefPromotable:(is_zefref_promotable_implementation, is_zefref_promotable_type_info),
         RT.InFrame:         (in_frame_imp, in_frame_tp),
         RT.ToGraphSlice:    (to_graph_slice_imp, to_graph_slice_tp),
@@ -105,12 +101,13 @@
         RT.O:               (o_implementation, o_type_info),
         RT.L:               (l_implementation, l_type_info),
         RT.Terminate:       (terminate_implementation, terminate_type_info),
-        RT.AssignValue:     (assign_value_imp, assign_value_tp),
+        RT.Assign:          (assign_imp, assign_tp),
         RT.ET:              (ET_implementation, ET_type_info),
         RT.RT:              (RT_implementation, RT_type_info),
         RT.AET:             (AET_implementation, AET_type_info),
         RT.BT:              (BT_implementation, BT_type_info),
         RT.FillOrAttach:    (fill_or_attach_implementation, fill_or_attach_type_info),
+        RT.SetField:        (set_field_implementation, set_field_type_info),
         RT.Assert:          (assert_implementation, assert_type_info),
         RT.HasOut:          (hasout_implementation, hasout_type_info),
         RT.HasIn:           (hasin_implementation, hasin_type_info),
@@ -140,7 +137,7 @@
         RT.RaeType:         (rae_type_implementation, rae_type_type_info),
         RT.AbstractType:    (abstract_type_implementation, abstract_type_type_info),
         RT.Root:            (root_imp, root_tp),
-        RT.Schema:          (schema_imp, schema_tp),
+        RT.Blueprint:       (blueprint_imp, None),
         RT.Z:               (Z_imp, Z_tp),
         RT.Docstring:       (docstring_imp, None),
         RT.SourceCode:      (source_code_imp, None),
@@ -173,8 +170,6 @@
         RT.Yo:              (yo_implementation, yo_type_info),
         
         RT.Sign:            (sign_imp, sign_tp),
-        RT.IfThenElse:      (if_then_else_imp, if_then_else_tp),
-        RT.IfThenElseApply:      (if_then_else_apply_imp, if_then_else_apply_tp),
         RT.Attempt:         (attempt_imp, attempt_tp),
         RT.Bypass:          (bypass_imp, bypass_tp),
         RT.Pattern:         (pattern_imp, pattern_tp),
@@ -186,6 +181,8 @@
         RT.Shuffle:         (shuffle_imp, shuffle_tp),
         RT.Slice:           (slice_imp, slice_tp),
         RT.Split:           (split_imp, split_tp),
+        RT.SplitLeft:       (split_left_imp, None),
+        RT.SplitRight:      (split_right_imp, None),
         RT.SplitIf:         (split_if_imp, None),
         RT.Graphviz:        (graphviz_imp, graphviz_tp),
         
@@ -223,7 +220,6 @@
         RT.Xor:                 (xor_imp, xor_tp),
         RT.Peel:                (peel_imp, peel_tp),
         RT.Match:               (match_imp, match_tp),
-        RT.MatchApply:          (match_apply_imp, match_apply_tp),
 
         RT.Sync:                (sync_imp, sync_tp),
         RT.Tag:                 (tag_imp, tag_tp),
@@ -256,11 +252,11 @@
         RT.InjectList:          (inject_list_imp, inject_list_tp),
 
         RT.ZasciiToAsg:         (zascii_to_asg_imp, zascii_to_asg_tp),
-        RT.ZasciiToScehma:      (zascii_to_schema_imp, zascii_to_schema_tp),
+        RT.ZasciiToFlatGraph:   (zascii_to_flatgraph_imp, zascii_to_flatgraph_tp,),
+        RT.ZasciiToBlueprintFg:  (zascii_to_blueprint_fg_imp, zascii_to_blueprint_fg_tp),
 
         RT.ReplaceAt:           (replace_at_imp, replace_at_tp),
         RT.RandomPick:          (random_pick_imp, random_pick_tp),
-        RT.PadToLength:         (pad_to_length_imp, pad_to_length_tp),
         RT.PadLeft:             (pad_left_imp, None),
         RT.PadRight:            (pad_right_imp, None),
         RT.PadCenter:           (pad_center_imp, None),
@@ -280,11 +276,13 @@
         RT.ToScreamingSnakeCase:(to_screaming_snake_case_imp, to_screaming_snake_case_tp),
 
 
-        RT.Call:                (call_implementation, call_type_info),
         RT.Unpack:              (unpack_implementation, unpack_type_info),
+        RT.IndexesOf:           (indexes_of_imp, None),
+        RT.GraphSliceIndex:     (graph_slice_index_imp, None),
 
         RT.MakeRequest:        (make_request_imp, make_request_tp),
         RT.Blake3:             (blake3_imp, blake3_tp),
+        RT.ValueHash:          (value_hash_imp, value_hash_tp),
         RT.ToZefList:          (to_zef_list_imp, to_zef_list_tp),
         RT.Transact:           (transact_imp, transact_tp),
 
@@ -299,4 +297,16 @@
         RT.BytesToBase64string:(bytes_to_base64string_imp, None),
         RT.IsBetween:          (is_between_imp, None),
         RT.If:                 (If_imp, None),
+        RT.Field:              (field_imp, None),
+        RT.Fields:             (fields_imp, None),
+        RT.Apply:              (apply_imp, None),
+        RT.SplitOnNext:        (split_on_next_imp, None),
+
+
+        RT.Examples:           (examples_imp, None),
+        RT.Signature:          (signature_imp, None),
+        RT.Tags:               (tags_imp, None),
+        RT.RelatedOps:         (related_ops_imp, None),
+        RT.OperatesOn:         (operates_on_imp, None), 
+        RT.UsedFor:            (used_for_imp, None),
 }
diff --git a/python/zef/core/op_implementations/func.py b/python/zef/core/op_implementations/func.py
index 3b794bb8..71e992dd 100644
--- a/python/zef/core/op_implementations/func.py
+++ b/python/zef/core/op_implementations/func.py
@@ -16,29 +16,33 @@
 from .._core import *
 
 ##############################
-# * Call
+# * Unpack
 #----------------------------
 
-# Call implements the laziness part of the op chain. It's use on the user-facing
-# side is discouraged, although zef functions will be converted to call ops.
+def unpack_implementation(inputs, f):
+    """
+    used to wrap an zefop or a function. If the function
+    takes multiple arguments, it is converted into a function
+    that takes one tuple or list as a single argument.
+    This list is unpacked into the function arguments.
 
-def call_implementation(input, f, *args):
-    from typing import Generator, Iterable, Iterator
-    if isinstance(input, Generator) or isinstance(input, Iterator):
-        input = [i for i in input]
-    return f(input, *args)
+    ---- Signature ----
+    (Tuple[T1, T2, ...], Func[T1, T2, ...][T] ) -> T
 
-def call_type_info(op, curr_type):
-    # TODO
-    return VT.Any
+    ---- Examples ----
+    >>> (42, 10) | unpack[subtract]      # => 32
 
-##############################
-# * Unpack
-#----------------------------
 
-def unpack_implementation(inputs, f):
+    ---- Tags ----
+    used for: control flow
+    related zefop: reverse_args
+    """
+    from typing import Generator
     if isinstance(inputs, tuple) or isinstance(inputs, list):
         return f(*inputs)
+    if isinstance(inputs, Generator):
+        return f(*tuple(inputs))
+
     elif isinstance(inputs, dict):
         # if this has wrapped a call[...] then we need to unpack the function
         # inside of it, as call itself does not handle kwargs
diff --git a/python/zef/core/op_implementations/graphviz.py b/python/zef/core/op_implementations/graphviz.py
index 6b5a2d4e..37316242 100644
--- a/python/zef/core/op_implementations/graphviz.py
+++ b/python/zef/core/op_implementations/graphviz.py
@@ -14,6 +14,7 @@
 
 from .. import *
 from .._ops import *
+from ..VT import Is, Any
 
 def graphviz_imp(zz, *flags):
     """
@@ -127,7 +128,7 @@ def hex_color_for_type_edges(some_type):
         return next(color_gen_edges)       
     
     graph_ctor_kwargs = ({} 
-        | if_then_else_apply[lambda _: KW.neato in flags][insert['engine']['neato']][identity]
+        | If[Is[lambda _: KW.neato in flags]][insert['engine']['neato']][identity]
         | collect
     )
 
@@ -154,7 +155,7 @@ def val_to_str(z):
                 if v is None: return 'None'
                 elif isinstance(v, str): return f'"{v}"' if len(v) < 20 else f'"{v[:18]}..."'
                 else: return str(v)
-            val_maybe = '' if isinstance(z, EZefRef) else (f"\n▷{val_to_str(z)}")
+            val_maybe = (f"\n▷{val_to_str(z)}") if isinstance(z, ZefRef) and not internals.is_delegate(z) else ''
             return f"AET.{AET(z)}{val_maybe}"        
         if BT(z)==BT.RELATION_EDGE:
             return f"RT.{str(RT(z))}"        
diff --git a/python/zef/core/op_implementations/implementation_typing_functions.py b/python/zef/core/op_implementations/implementation_typing_functions.py
index 9b4d9bd3..c52f9e74 100644
--- a/python/zef/core/op_implementations/implementation_typing_functions.py
+++ b/python/zef/core/op_implementations/implementation_typing_functions.py
@@ -32,8 +32,8 @@
 import itertools
 from typing import Generator, Iterable, Iterator
 
-zef_types = {VT.Graph, VT.ZefRef, VT.EZefRef}
-ref_types = {VT.ZefRef, VT.EZefRef}
+zef_types = [VT.Graph, VT.ZefRef, VT.EZefRef]
+ref_types = [VT.ZefRef, VT.EZefRef]
 
 
 #--utils---
@@ -81,15 +81,16 @@ def verify_zef_list(z_list: ZefRef):
     >>> <example one-liner 1>       # => <result of the one-liner (a value)>
     >>> <example one-liner 2>       # <comment on the behaviour (not a value)>
  
-    <description of following example>
+    >>> # <description of following example>
     >>> <example one-liner with multiline result>
-    ... <result line 1>
-    ... <result line 2>
+    <result line 1>
+    <result line 2>
  
-    <description of following example>
-    >>> <multiline example line 1>
-    >>> <multiline example line 2>
-    >>> <multiline example line 3>  # => <result of multiline example>
+    >>> # <description of following example>
+    >>> <multiline example line 1>    
+    ... <multiline example line 2>
+    ... <multiline example line 3>  
+    <result of multiline example>
  
     ---- Signature ----
     <method 1: input types as tuple> -> <output type>
@@ -174,18 +175,18 @@ def on_implementation(g, op):
     the very end.
 
     ---- Examples ----
-    >>> g | on[value_assigned[AET.String]]              # value_assigned[z3]['hello!']      c.f. with action: assign_value[z3]['hello!']
+    >>> g | on[assigned[AET.String]]                    # assigned[z3]['hello!']      c.f. with action: assign[z3]['hello!']
     >>> g | on[terminated[z2]]                          # terminated[z2], followed by completion_event
     >>> g | on[instantiated[ET.Foo]]                    # instantiated[z5]
-    >>> 
-    >>> # listening for new relations
-    >>> # old syntax:  zz | subscribe[on_instantiation[outgoing][RT.Foo]][my_callback]       old syntax
+     
+    # listening for new relations
+    # old syntax:  zz | subscribe[on_instantiation[outgoing][RT.Foo]][my_callback]       old syntax
     >>> g | on[ instantiated[(zz, RT.Foo, Z)] ]         # Z matches on anything, i.e. takes on role of "_"
-    >>>
+    
     >>> One can also add more precise requirements
     >>> g | on[ instantiated[(zz, RT.Foo, ET.Bar)] ]    # an element "instantiated[z_rel]" is pushed into the stream. Instances are represented by a single ZefRef, types by a triple for relations
     >>> g | on[ terminated[(zz, RT.Foo, Z)] ]
-    >>>
+    
     >>> g | on[terminated[z_rel]]
 
     ---- Signature ----
@@ -197,8 +198,8 @@ def on_implementation(g, op):
 
 
     """
-    assert isinstance(op, ZefOp)
-    assert len(op.el_ops) == 1
+    assert isinstance(op, ValueType_)
+    assert len(absorbed(op)) == 1
     from ...pyzef import zefops as internal
     from ..fx import FX, Effect
 
@@ -206,44 +207,64 @@ def on_implementation(g, op):
     sub_decl = internal.subscribe[internal.keep_alive[True]]
     
     if isinstance(g, Graph):
-        op_kind = op.el_ops[0][0]
-        op_args = op.el_ops[0][1]        
-        if op_kind in {RT.Terminated,RT.Instantiated}:
-            selected_types = {RT.Terminated: (internal.on_termination, terminated), RT.Instantiated: (internal.on_instantiation, instantiated)}[op_kind]
+        op_kind = without_absorbed(op)
+        op_args = absorbed(op)      
+        if op_kind in {Instantiated, Terminated}:
+            selected_types = {Terminated: (internal.on_termination, terminated), Instantiated: (internal.on_instantiation, instantiated)}[op_kind]
             
             if not isinstance(op_args[0], tuple):
                 rae_or_zr = op_args[0]
                 # Type 1: a specific entity i.e on[terminated[zr]]  !! Cannot be on[instantiated[zr]] because doesnt logically make sense !!
                 if isinstance(rae_or_zr, ZefRef): 
-                    assert op_kind == RT.Terminated, "Cannot listen for a specfic ZefRef to be instantiated! Doesn't make sense."
-                    def filter_func(root_node): root_node | frame | to_tx  | terminated | filter[lambda x: to_ezefref(x) == to_ezefref(rae_or_zr)] |  for_each[lambda x: LazyValue(terminated[x]) | push[stream] | run ] 
+                    assert op_kind == Terminated, "Cannot listen for a specfic ZefRef to be instantiated! Doesn't make sense."
+                    def filter_func(root_node): root_node | frame | to_tx | events[Terminated] | filter[lambda x: to_ezefref(absorbed(x)[0]) == to_ezefref(rae_or_zr)] |  for_each[lambda x: LazyValue(x) | push[stream] | run ] 
                     sub_decl = sub_decl[filter_func]
                     sub = g | sub_decl                
                 # Type 2: any RAE  i.e on[terminated[ET.Dog]] or on[instantiated[RT.owns]] 
                 elif type(rae_or_zr) in {AtomicEntityType, EntityType, RelationType}: 
-                    def filter_func(root_node): root_node | frame | to_tx | selected_types[1] | filter[lambda x: rae_type(x) == rae_or_zr] |  for_each[lambda x: LazyValue(selected_types[1][x]) | push[stream] | run ] 
+                    def filter_func(root_node): root_node | frame | to_tx | events[op_kind] | filter[lambda x: rae_type(absorbed(x)[0]) == rae_or_zr] |  for_each[lambda x: LazyValue(x) | push[stream] | run ] 
                     sub_decl = sub_decl[filter_func]
                     sub = g | sub_decl
                 else:
-                    raise Exception(f"Unhandled type in on[{selected_types[1]}] where the input was {rae_or_zr}")
+                    raise Exception(f"Unhandled type in on[{op_kind}] where the input was {rae_or_zr}")
 
             # Type 3: An instantiated or terminated relation outgoing or incoming from a specific zr 
             elif len(op_args[0]) == 3: 
-                src, rt, _ = op_args[0] # TODO don't ignore matching on the Target! To be implemented!
-                sub_decl = sub_decl[selected_types[0][internal.outgoing][rt]][lambda x: LazyValue(selected_types[1][x]) | push[stream] | run] # TODO: is it always outgoing?
-                sub = src | sub_decl            
+                src, rt, trgt = op_args[0] 
+                # sub_decl = sub_decl[selected_types[0][internal.outgoing][rt]][lambda x: LazyValue(selected_types[1][x]) | push[stream] | run] # TODO: is it always outgoing?
+                # sub = src | sub_decl         
+                def triple_filter(z):    
+                    def src_or_trgt_filter(rae, rae_filter):
+                        if isinstance(rae_filter, ZefRef):
+                            return to_ezefref(rae) == to_ezefref(rae_filter)
+                        elif isinstance(rae_filter, (EntityType, AtomicEntityType, RelationType)):
+                            return rae_type(rae) == rae_filter
+                        elif isinstance(rae_filter, ValueType_):
+                            return is_a(rae, rae_filter)
+                        else:
+                            raise ValueError(f"Expected source or target filters to be ZefRef, RaeType, or ValueType but got {type(rae_filter)} instead.")
+
+                    # First check on the RT itself
+                    if rae_type(z) != rt: return False
+                    # Checks on both source and target
+                    return src_or_trgt_filter(source(z), src) and src_or_trgt_filter(target(z), trgt)
+
+                def filter_func(root_node): root_node | frame | to_tx | events[op_kind] | filter[lambda x: triple_filter(absorbed(x)[0])] | for_each[lambda x: run(LazyValue(x) | push[stream]) ]  
+                sub_decl = sub_decl[filter_func]
+                sub = g | sub_decl              
             else:
                 raise Exception(f"Unhandled type in on[{selected_types[1]}] where the curried args were {op_args}")
-        elif op_kind == RT.ValueAssigned:
+        elif op_kind == Assigned:
             assert len(op_args) == 1
             aet_or_zr = op_args[0]
-            # Type 1: a specific zefref to an AET i.e on[value_assigned[zr_to_aet]]
+            # Type 1: a specific zefref to an AET i.e on[assigned[zr_to_aet]]
             if isinstance(aet_or_zr, ZefRef): 
-                sub_decl = sub_decl[internal.on_value_assignment][lambda x: LazyValue(value_assigned[x][x|value|collect]) | push[stream] | run]
-                sub = aet_or_zr | sub_decl
-            # Type 2: any AET.* i.e on[value_assigned[AET.String]]
+                def filter_func(root_node): root_node | frame | to_tx | events[Assigned] | filter[lambda x: to_ezefref(absorbed(x)[0]) == to_ezefref(aet_or_zr)] |  for_each[lambda x: run(LazyValue(x) | push[stream]) ]  
+                sub_decl = sub_decl[filter_func]
+                sub = g | sub_decl
+            # Type 2: any AET.* i.e on[assigned[AET.String]]
             elif isinstance(aet_or_zr, AtomicEntityType): 
-                def filter_func(root_node): root_node | frame | to_tx | value_assigned | filter[aet_or_zr] | map[lambda x: value_assigned[x][x|value|collect]] |  for_each[lambda x: run(LazyValue(x) | push[stream]) ]  
+                def filter_func(root_node): root_node | frame | to_tx | events[Assigned] | filter[lambda x: rae_type(absorbed(x)[0]) == aet_or_zr] |  for_each[lambda x: run(LazyValue(x) | push[stream]) ]  
                 sub_decl = sub_decl[filter_func]
                 sub = g | sub_decl        
         else:
@@ -267,7 +288,7 @@ def transpose_imp(iterable):
 
     ---- Examples ----
     >>> [ [2,3,4], [5,6,7] ]                    # => [ [2, 5], [3,6], [4,7] ]
-    >>> 
+     
     >>> # terminates upon the shortest one:
     >>> [ range(2, infinity), [5,6], [15,16,17] ]    # => [[2, 5, 15], [3, 6, 16]]
 
@@ -297,11 +318,13 @@ def match_imp(item, patterns):
 
     ---- Examples ----
     >>> -9 | match[
-    >>>     (Is[less_than[-10]], lambda x: f'it is a freezing {x} degrees'),
-    >>>     (Is[less_than[10]], lambda x: f'somewhat cold: {x} degrees'),
-    >>>     (Is[greater_than_or_equal[20]], lambda x: f'warm: {x}'),
-    >>>     (Any, lambda x: f'something else {x}'),
-    >>> ] | collect                            => 'somewhat cold: -9 degrees'
+    ...     ({24, 42}, lambda x: f'a very special temperature'),
+    ...     (Is[less_than[-10]], lambda x: f'it is a freezing {x} degrees'),
+    ...     (Is[less_than[10]], lambda x: f'somewhat cold: {x} degrees'),
+    ...     (Is[greater_than_or_equal[20]], lambda x: f'warm: {x}'),
+    ...     (Any, lambda x: f'something else {x}'),
+    ... ] | collect                             
+    'somewhat cold: -9 degrees'
 
     ---- Arguments ----
     item: the incoming value
@@ -317,9 +340,12 @@ def match_imp(item, patterns):
     """
     for tp, f_to_apply in patterns:
         try:
-            if is_a(item, tp): return f_to_apply(item)
+            if( (item in tp) 
+                if isinstance(tp, set)    # if a set is passed in: check membership directly
+                else is_a(item, tp)
+                ): return f_to_apply(item)
         except Exception as e:            
-            raise RuntimeError(f'\nError within `match` zefop case predicate: `{tp}`  `applying function: {f_to_apply}`: {e}')
+            raise RuntimeError(f'\nError within `match` zefop case predicate: `{tp}`  `applying function: {f_to_apply}`: {e}') from e
     raise RuntimeError(f'None of the specified patterns matched for value {item} in "match operator": pattern: {patterns}')
     
 
@@ -331,47 +357,13 @@ def match_tp(op, curr_type):
 
 
 
-
-def match_apply_imp(item, patterns):
-    """
-    This is apply version of match operator. Where the pattern tuple's second item contains a function to be applied
-    on the inputed item.
-
-    ---- Examples ----
-    >>> -9 | match[
-    >>>     (less_than[-10], add[10]),
-    >>>     (less_than[0], multiply[2]),
-    >>>     (greater_than_or_equal[0], add[1]),
-    >>> ] | collect                            => -18
-
-    ---- Arguments ----
-    item: the incoming value
-    patterns: (T, T) -> Bool
-
-    ---- Signature ----
-    (T, (T->Any)) -> T
-
-    ---- Tags ----
-    - used for: control flow
-    - used for: logic
-    - used for: function application
-    """
-    for pred, applied_func in patterns:
-        if pred(item):
-            assert callable(applied_func)
-            return applied_func(item)
-    raise RuntimeError(f'None of the specified patterns matched for value {item} in "match operator": pattern: {patterns}')
-    
-
-def match_apply_tp(op, curr_type):
-    return VT.Any
 #---------------------------------------- peel -----------------------------------------------
 def peel_imp(el, *args):
-    from ..fx.fx_types import _Effect_Class
     if isinstance(el, ValueType_):
         return el.nested()
-    elif isinstance(el, _Effect_Class):
-        return el.d
+    elif isinstance(el, dict):
+        print('deprecation warning: `peel` called on a dictionary. This probably was an Effect before and is no longer needed.')
+        return el
     elif isinstance(el, ZefOp):
         # TODO !!!!!!!!!!!!! Change this to always return elementary ZefOps and to use list(el) to get current behavior
         if len(el.el_ops) > 1:
@@ -432,10 +424,11 @@ def concat_implementation(v, first_curried_list_maybe=None, *args):
     ---- Examples ----
     >>> # A) all passed in via a List of Lists (no args curried into concat op)
     >>> [
-    >>>     [1,2,3],                                    
-    >>>     ['a', 'b', 'c', 'd'],
-    >>> ] | concat                                      # =>  [1, 2, 3, 'a', 'b', 'c', 'd']
-    >>> 
+    ...     [1,2,3],                                    
+    ...     ['a', 'b', 'c', 'd'],
+    ... ] | concat                                      
+    [1, 2, 3, 'a', 'b', 'c', 'd']
+     
     >>> # B) One list piped in, other lists to concatenated curried into op
     >>> [1,2,3] | concat[ (42,43) ][('a','b','c')]      # =>  [1, 2, 3, 42, 43, 'a', 'b', 'c']
 
@@ -675,11 +668,19 @@ def get_in_imp(d: dict, path, default_val=VT.Error):
     ---- Examples ----
     >>> {'a': 1, 'b': {'c': 1}} | get_in[('b', 'c')]   # => 1
     >>> {'a': 1, 'b': {'c': 1}} | get_in[('b', 'wrong_key')][42]   # => 42
+
+    ---- Tags ----
+    - related zefop: get
+    - related zefop: remove_in
+    - related zefop: update_in
+    - related zefop: insert_in
+    - related zefop: get_in
+    - operates on: Dict
     """
     assert isinstance(path, list) or isinstance(path, tuple)
-    if len(path) == 0: return d
-    if type(d) != dict: return default_val
-    return get_in(d.get(path[0], default_val), path[1:], default_val)
+
+    from ..pure_utils import get_in_pure
+    return get_in_pure(d, path, default_val)
 
     
 def get_in_tp(d_tp, path_tp, default_val_tp):
@@ -691,8 +692,18 @@ def get_in_tp(d_tp, path_tp, default_val_tp):
 #---------------------------------------- insert_in -----------------------------------------------
 def insert_in_imp(d: dict, path, value):
     """
+    Create a new dicitonary from an old one by inserting one value
+    for a given nested path.
+
     ---- Examples ----
     >>> {'a': 1, 'b': {'c': 1}} | insert_in[('b', 'd')][42]   # => {'a': 1, 'b': {'c': 1, 'd': 42}} 
+  
+    ---- Tags ----
+    - related zefop: insert
+    - related zefop: get_in
+    - related zefop: remove_in
+    - related zefop: update_in
+    - operates on: Dict
     """
     assert isinstance(path, list) or isinstance(path, tuple)
     res = {**d}    
@@ -753,6 +764,13 @@ def remove_in_tp(d_tp, path_tp):
 #---------------------------------------- update_in -----------------------------------------------
 def update_in_imp(d: dict, path, func_to_update):
     """
+    Given a dictionary, a tuple describing a path into
+    the dictionary and an update function, 
+    this returns a new dictionary with the element point to
+    replaced with the value obtained by applying the update 
+    function to the previous element.
+    The original dictionary is not mutated.
+
     ---- Examples ----
     >>> {'a': 1, 'b': {'c': 1}} | update_in[('b', 'c')][add[1]]   # => {'a': 1, 'b': {'c': 2}} 
 
@@ -957,7 +975,8 @@ def update_imp(d: dict, k, fn):
     (to be consistent with "remove_at" for Lists and 
     disambiguate acting on the value)
 
-    {'a': 5, 'b': 6} | update['a'][add[10]]   # => {'a': 15, 'b': 6}
+    ---- Examples ----
+    >>> {'a': 5, 'b': 6} | update['a'][add[10]]   # => {'a': 15, 'b': 6}
 
     ---- Tags ----
     - related zefop: update_at
@@ -982,8 +1001,9 @@ def remove_at_imp(v, *nn):
     as Python's list remove, searches for the first 
     occurrence of that value and removes it.
 
-    ['a', 'b', 'c'] | remove_at[1]       # => ['a', 'c']
-    ['a', 'b', 'c'] | remove_at[1][0]    # => ['c']
+    ---- Examples ----
+    >>> ['a', 'b', 'c'] | remove_at[1]       # => ['a', 'c']
+    >>> ['a', 'b', 'c'] | remove_at[1][0]    # => ['c']
 
     ---- Signature ----
     List[T] & Length[Z] -> List[T] & Length[Z-1]
@@ -1011,9 +1031,10 @@ def interleave_imp(v, first_curried_list_maybe=None, *args):
     ---- Examples ----
     >>> # Either called with a list of of lists (or equivalent for streams)
     >>> [
-    >>>     [1,2,3],
-    >>>     ['a', 'b', 'c', 'd']        
-    >>> ] | interleave          # =>  [1, 'a', 2, 'b', 3, 'c']
+    ...    [1,2,3],
+    ...    ['a', 'b', 'c', 'd']        
+    ... ] | interleave          
+    [1, 'a', 2, 'b', 3, 'c']
     >>> 
     >>> # or with other lists to interleave with being curried in
     >>> [1,2,3] | interleave[42 | repeat]       # => [1, 42, 2, 42, 3, 42]
@@ -1200,6 +1221,8 @@ def sliding_imp(iterable, window_size: int, stride_step: int=1):
     many elements are taken forward in each step.
     Default stride_step if not specified otherwise: 1.
 
+    implementation follows Scala's, see https://stackoverflow.com/questions/32874419/scala-slidingn-n-vs-groupedn
+
     ---- Examples ----
     >>> range(5) | sliding[3]       # [(0, 1, 2), (1, 2, 3), (2, 3, 4)]
     >>> range(5) | sliding[3][2]    # [(0, 1, 2), (2, 3, 4)]
@@ -1212,8 +1235,6 @@ def sliding_imp(iterable, window_size: int, stride_step: int=1):
     - related zefop: chunk
     - related zefop: slice
     - operates on: List
-
-    implementation follows Scala's, see https://stackoverflow.com/questions/32874419/scala-slidingn-n-vs-groupedn
     """
     it = iter(iterable)
     try:
@@ -1311,8 +1332,8 @@ def reverse_args_imp(flow_arg, op, *args):
     ---- Tags ----
     - level: advanced
     - used for: control flow
-    - operates on: ZefOps
-    - operates on: Functions
+    - operates on: ZefOp
+    - operates on: Function
     - related zefop: func
     - related zefop: apply_functions
     """
@@ -1324,9 +1345,16 @@ def reverse_args_imp(flow_arg, op, *args):
 
 def insert_into_imp(key_val_pair, x):
     """
-    Unclear whether we need this. To insert a key value
-    pair into a dictionary, one could cast to a dict and use merge.
-    But this may be shorter.
+    For Dicts:
+    Given a dictionary as a curried in argument and the key 
+    value pair as the data flow argument, return a new dictionary
+    with the inserted value.
+
+    For Lists:
+    the dataflow argument is a tuple (pos, val), where
+    val will be inserted into the given list at position pos.    
+    For Lists this operator is equivalent to insert_at,
+    but with the argument oder flipped.
 
     This function could also be used on Lists. 
     
@@ -1336,6 +1364,15 @@ def insert_into_imp(key_val_pair, x):
     ---- Signature ----
     ((T1, T2), Dict[T3][T4]) -> Dict[T1|T3][T2|T4]
 
+    ---- Tags ----
+    - level: advanced
+    - operates on: Dict
+    - operates on: List
+    - related zefop: insert
+    - related zefop: insert_at
+    - related zefop: merge
+    - related zefop: apply_functions
+
     # TODO: make this work: (10, 'a') | insert_into[range(10) | map[add[100]]] | take[5] | c
 
     """
@@ -1456,6 +1493,7 @@ def get_field_imp(obj, field):
     
     ---- Tags ----
     - related zefop: get
+    - operates on: Python objects
     """
     return getattr(obj, field)
 
@@ -1481,6 +1519,7 @@ def enumerate_imp(v):
     - used for: list manipulation
     - used for: string manipulation
     - used for: lazy transformation
+    - operates on: List
     """
     import builtins
     return builtins.enumerate(v)
@@ -1505,6 +1544,7 @@ def items_imp(d):
     ---- Tags ----
     - used for: dict manipulation
     - used for: list manipulation
+    - operates on: Dict
     """
     return tuple(d.items())
 
@@ -1527,7 +1567,7 @@ def values_imp(d):
 
     ---- Tags ----
     - used for: dict manipulation
-    - used for: list manipulation
+    - operates on: Dict
     """
     return tuple(d.values())
 
@@ -1550,7 +1590,7 @@ def keys_imp(d):
 
     ---- Tags ----
     - used for: dict manipulation
-    - used for: list manipulation
+    - operates on: Dict
     """
     return tuple(d.keys())
 
@@ -1577,6 +1617,7 @@ def reverse_imp(v):
     ---- Tags ----
     - used for: list manipulation
     - used for: stream manipulation
+    - operates on: List
     """
     from typing import Generator
     if isinstance(v, Generator): return (tuple(v))[::-1]
@@ -1598,8 +1639,17 @@ def cycle_imp(iterable, n=None):
     Given a list, this wll produce another list loft the same structure
     that cycles thorugh all elements of the original list n times.
     
-    e.g. [2,3] | cycle[3]  -> [2,3,2,3,2,3]
-    cycle[None]:     means never terminate
+    ---- Examples ----
+    >>> [2,3] | cycle[3]  # => [2,3,2,3,2,3]
+    >>> [2,3] | cycle     # => [2,3,2,3,2,3,...    # never terminates
+
+    ---- Signature ----
+    (List[T], Int) -> List[T]
+
+    ---- Tags ----
+    - used for: list manipulation
+    - used for: stream manipulation
+    - operates on: List
     """
     if n==0:
         return
@@ -1625,6 +1675,18 @@ def cycle_tp(iterable_tp, n_tp):
 
 #---------------------------------------- repeat -----------------------------------------------
 def repeat_imp(iterable, n=None):
+    """
+    Repeat an element a given number of times 
+    None (default) means infinite).
+
+    ---- Examples ----
+    >>> 42 | repeat[3]    # => [42,42,42]
+    >>> (1,'a') | repeat  # => [(1,'a'), (1,'a'), ...]    # infinite sequence
+
+    ---- Tags ----
+    - related zefop: cycle
+    - operates on: Any
+    """
     import itertools
     if isinstance(iterable, Generator) or isinstance(iterable, Iterator): iterable = [i for i in iterable]
     return itertools.repeat(iterable) if n is None else itertools.repeat(iterable, n)
@@ -1637,6 +1699,23 @@ def repeat_tp(iterable_tp, n_tp):
 
 #---------------------------------------- contains -----------------------------------------------
 def contains_imp(x, el):
+    """
+    Check whether an element is in a given
+    set or iterable. Operator for of Python's
+    `in`.
+
+    ---- Examples ----
+    >>> {42, 43} | contains[42]         # => 42
+    >>> g | contains[my_entity_ref]     # => 
+
+    ---- Tags ----
+    - related zefop: contained_in
+    - operates on: List
+    - operates on: Set
+    - operates on: Dict
+    - operates on: Graph
+    - operates on: FlatGraph
+    """
     return el in x    
                         
 
@@ -1694,7 +1773,7 @@ def all_imp(*args):
     (GraphLike, Type) -> List[EZefRef]
  
     ---- Tags ----
-    - used for: predicates
+    - used for: predicate
     """
 
     import builtins
@@ -1772,6 +1851,13 @@ def all_imp(*args):
         z, = args
         assert internals.is_delegate(z)
         return z | pyzefops.instances
+
+    import types
+    if isinstance(args[0], types.ModuleType):
+        assert len(args) == 2
+        assert isinstance(args[1], ValueType_)
+        list_of_module_values = [args[0].__dict__[x] for x in dir(args[0]) if not x.startswith("_") and not isinstance(getattr(args[0], x), types.ModuleType)]
+        return list_of_module_values | filter[args[1]] | collect
     
     # once we're here, we interpret it like the Python "all"
     v = args[0]
@@ -1814,8 +1900,18 @@ def any_tp(v_tp):
 #---------------------------------------- join -----------------------------------------------
 def join_imp(list_of_strings: VT.List[VT.String], x=''):
     """ 
-    join a list of strings with a binding character.
+    Join a list of strings with a binding character.
+
+    ---- Examples ----
     >>> ['foo', 'bar'] | join['-']              # => 'foo-bar'
+
+    ---- Signature ----    
+    (List[String], String) -> String
+
+    ---- Tags ----
+    - related zefop: concat
+    - related zefop: insert_at
+    - operates on: String
     """
     return x.join(list_of_strings)
 
@@ -1827,6 +1923,26 @@ def join_tp(v, x):
 
 #---------------------------------------- trim_left -----------------------------------------------
 def trim_left_imp(v, el_to_trim):
+    """
+    Removes all contiguous occurrences of a specified 
+    element / character from the left side of a list / string.
+
+    ---- Examples ----
+    >>> '..hello..' | trim_left['.']            # => ['hello..']
+
+    ---- Signature ----
+    (List[T], T) -> List[T]
+    (String, String) -> String
+
+    ---- Tags ----
+    - related zefop: trim
+    - related zefop: trim_right
+    - related zefop: split
+    - operates on: List
+    - operates on: String
+    - used for: list manipulation
+    - used for: string manipulation
+    """
     if isinstance(v, str):        
         return v.lstrip(el_to_trim)
     def wrapper():
@@ -1850,6 +1966,26 @@ def trim_left_tp(v_tp, el_to_trim_tp):
     
 #---------------------------------------- trim_right -----------------------------------------------
 def trim_right_imp(v, el_to_trim):
+    """
+    Removes all contiguous occurrences of a specified 
+    element / character from the right side of a list / string.
+
+    ---- Examples ----
+    >>> '..hello..' | trim_right['.']            # => ['..hello']
+
+    ---- Signature ----
+    (List[T], T) -> List[T]
+    (String, String) -> String
+
+    ---- Tags ----
+    - related zefop: trim
+    - related zefop: trim_left
+    - related zefop: split
+    - operates on: List
+    - operates on: String
+    - used for: list manipulation
+    - used for: string manipulation
+    """
     import itertools 
     if isinstance(v, str):
         return v.rstrip(el_to_trim)
@@ -1867,6 +2003,26 @@ def trim_right_tp(v_tp, el_to_trim_tp):
 
 #---------------------------------------- trim -----------------------------------------------
 def trim_imp(v, el_to_trim):
+    """
+    Removes all contiguous occurrences of a specified 
+    element / character from both sides of a list / string.
+
+    ---- Examples ----
+    >>> '..hello..' | trim['.']            # => ['..hello']
+
+    ---- Signature ----
+    (List[T], T) -> List[T]
+    (String, String) -> String
+
+    ---- Tags ----
+    - related zefop: trim_left
+    - related zefop: trim_right
+    - related zefop: split
+    - operates on: List
+    - operates on: String
+    - used for: list manipulation
+    - used for: string manipulation
+    """
     import itertools 
     if isinstance(v, str):
         return v.strip(el_to_trim)
@@ -1884,6 +2040,26 @@ def trim_tp(v_tp, el_to_trim_tp):
 
 #---------------------------------------- tap -----------------------------------------------
 def tap_imp(x, fct):
+    """
+    An operator mostly used for debugging and testing
+    purposes: allows inserting an impure function into
+    a pipeline.
+    In contrast to apply/map it is not the output of
+    provided function which is forwarded, but the original 
+    input. It is therefore ever only of any use to
+    insert an impure function into tap, since the return 
+    value is discarded.
+
+    ---- Examples ----
+    >>> 41 | add[1] | tap[print] | multiply[2] | collect    # prints 42 and the expression returns 84
+
+    ---- Tags ----
+    - used for: debugging
+    - related zefop: apply
+    """
+    # As we can receive a generator, we need to collect it up first before continuing
+    if isinstance(x, Generator):
+        x = list(x)
     fct(x)
     return x
         
@@ -2058,7 +2234,7 @@ def absorbed_imp(x):
     
     ---- Tags ----
     - used for: control flow
-    - operates on: ZefOps, Value Types, Entity, Relation, AtomicEntity, ZefRef, EZefRef
+    - operates on: ZefOp, Value Types, Entity, Relation, AtomicEntity, ZefRef, EZefRef
     - related zefop: without_absorbed
     - related zefop: inject
     - related zefop: inject_list
@@ -2102,7 +2278,7 @@ def without_absorbed_imp(x):
     
     ---- Tags ----
     - used for: control flow
-    - operates on: ZefOps, Value Types, Entity, Relation, AtomicEntity, ZefRef, EZefRef
+    - operates on: ZefOp, Value Types, Entity, Relation, AtomicEntity, ZefRef, EZefRef
     - related zefop: absorbed
     - related zefop: inject
     - related zefop: inject_list
@@ -2132,6 +2308,13 @@ def without_absorbed_imp(x):
             new_kw = Keyword(x.value)
             return new_kw
 
+    elif isinstance(x, Delegate):
+        if '_absorbed' not in x.__dict__:
+            return x
+        else:
+            new_delegate = Delegate(x.order, x.item)
+            return new_delegate
+
     elif isinstance(x, ZefOp):
         if len(x.el_ops) != 1: 
             return Error(f'"without_absorbed_imp" can only be called on an elementary ZefOp, i.e. one of length 1. It was called on x={x}')
@@ -2184,6 +2367,7 @@ def product_imp(v):
     ---- Tags ----
     used for: maths
     operates on: List
+    related zefop: cartesian_product
     related zefop: multiply
     related zefop: sum
     """
@@ -2192,7 +2376,7 @@ def product_imp(v):
 
 
 #---------------------------------------- add -----------------------------------------------
-def add_imp(a, second=None, *args):
+def add_imp(a, b):
     """
     Adds two elements. Neither is a list.
     Don't use this when summing up a list, use `sum`
@@ -2206,16 +2390,16 @@ def add_imp(a, second=None, *args):
 
     ---- Tags ----
     used for: maths
+    operates on: Int
+    operates on: Float
     related zefop: sum
     related zefop: subtract
     related zefop: multiply
     related zefop: divide
     """
-    from functools import reduce
-    if second is None:
-        print(f"Warning: add was used for list {a}. This use will be deprecated: use `sum` here.")
-        return reduce(lambda x, y: x + y, a)
-    return reduce(lambda x, y: x + y, [a, second, *args])
+    if type(a) in {list, tuple} or type(b) in {list, tuple}:
+        raise TypeError(f"`add` is a binary operator, i.e. always takes two arguments. If you want to sum up all elements in a list, use `sum`")
+    return a+b
     
     
 def add_tp(a, second, *args):
@@ -2224,11 +2408,31 @@ def add_tp(a, second, *args):
     
     
 #---------------------------------------- subtract -----------------------------------------------
-def subtract_imp(a, second=None):    
-    if second is None:
-        assert len(a) == 2
-        return a[0]-a[1]
-    return a-second
+def subtract_imp(a, b):
+    """
+    Binary operator to subtract two elements. Neither is a list.
+    If am operator is needed that can act on a tuple of length 2,
+    this can be wrapped in `unpack`.
+
+    ---- Examples ----
+    44 | subtract[2]               # => 42
+    (42, 10) | unpack[subtract]    # => 32
+
+    ---- Signature ----
+    (T1, T2) -> T1 | T2
+
+    ---- Tags ----
+    used for: maths
+    operates on: Int
+    operates on: Float
+    related zefop: add
+    related zefop: multiply
+    related zefop: divide
+    related zefop: unpack
+    """
+    if type(a) in {list, tuple} or type(b) in {list, tuple}:
+        raise TypeError(f"`subtract` is a binary operator, i.e. always takes two arguments.")
+    return a-b
     
     
 def subtract_tp(a, second):
@@ -2240,9 +2444,24 @@ def subtract_tp(a, second):
 #---------------------------------------- multiply -----------------------------------------------
 def multiply_imp(a, second=None, *args):
     """ 
-    Multiplies a list of numbers.
-    We don't use 'product' to name this, as that term is too overloaded.
-    e.g. in Python itertools this denotes the Cartesian product.
+    Binary operator only. For a list of numbers, use `product`.
+
+    ---- Examples ----
+    >>> 2 | multiply[3]    # => 6
+
+    ---- Signature ----
+    (Int, Int) - > Int
+    (Float, Float) - > Float
+
+    ---- Tags ----
+    used for: maths
+    operates on: Int
+    operates on: Float
+    related zefop: product
+    related zefop: add
+    related zefop: divide
+    related zefop: subtract
+    related zefop: unpack
     """
     from functools import reduce
     if second is None:
@@ -2256,10 +2475,31 @@ def multiply_tp(a, second, *args):
 
 #---------------------------------------- divide -----------------------------------------------
 def divide_imp(a, second=None):    
-    if second is None:
-        assert len(a) == 2
-        return a[0]/a[1]
-    return a/second
+    """
+    Binary operator to divide two elements. Neither is a list.
+    If am operator is needed that can act on a tuple of length 2,
+    this can be wrapped in `unpack`.
+
+    ---- Examples ----
+    10 | divide[2]                # => 5
+    (10, 2) | divide[subtract]    # => 5
+
+    ---- Signature ----
+    (T1, T2) -> Float
+
+    ---- Tags ----
+    used for: maths
+    operates on: Int
+    operates on: Float
+    related zefop: add
+    related zefop: multiply
+    related zefop: add
+    related zefop: subtract
+    related zefop: unpack
+    """
+    if type(a) in {list, tuple} or type(b) in {list, tuple}:
+        raise TypeError(f"`subtract` is a binary operator, i.e. always takes two arguments.")
+    return a-b
     
     
 def divide_tp(a, second):
@@ -2273,12 +2513,21 @@ def divide_tp(a, second):
 def mean_imp(v):
     """
     Calculates the arithmetic mean of a given List / Set of numbers.
+   
+    ---- Examples ----
+    >>> [3,7,20] | mean    # => 10
 
     ---- Signature ----
     List[Int] -> Float
     Set[Int] -> Float
     List[Float] -> Float
     Set[Float] -> Float
+
+    ---- Tags ----
+    used for: maths
+    operates on: List    
+    operates on: Set
+    related zefop: variance
     """
     return sum(v) / length(v)
 
@@ -2292,11 +2541,20 @@ def variance_imp(v):
     """
     Calculates the variance of a given List / Set of numbers.
 
+    ---- Examples ----
+    >>> [-1,0,1] | variance    # => 0.6666..
+
     ---- Signature ----
     List[Int] -> Float
     Set[Int] -> Float
     List[Float] -> Float
     Set[Float] -> Float
+
+    ---- Tags ----
+    used for: maths
+    operates on: List    
+    operates on: Set
+    related zefop: mean
     """
     le = length(v)
     return sum( (x*x for x in v) )/le - (sum(v)/le)**2
@@ -2308,6 +2566,23 @@ def variance_tp(v):
 
 #---------------------------------------- power -----------------------------------------------
 def power_imp(x, b):
+    """
+    Take the first argument to the power (exponential) of the second argument.
+   
+    ---- Examples ----
+    >>> 2 | power[8]    # => 256
+
+    ---- Signature ----
+    (Int, Int) -> Int
+    (Float | Int, Float | Int) -> Float
+
+    ---- Tags ----
+    used for: maths
+    operates on: Int    
+    operates on: Float    
+    related zefop: exponential
+    related zefop: multiply
+    """
     return x**b
 
 def power_tp(op, curr_type):
@@ -2316,6 +2591,22 @@ def power_tp(op, curr_type):
 
 #---------------------------------------- exponential -----------------------------------------------
 def exponential_imp(x):
+    """
+    Take the first argument to the power of the second argument.
+   
+    ---- Examples ----
+    >>> 2 | exponential[8]    # => 256
+
+    ---- Signature ----
+    (Float, Float) -> Float
+
+    ---- Tags ----
+    used for: maths
+    
+    related zefop: logarithm
+    related zefop: power
+    related zefop: multiply
+    """
     import math
     return math.exp(x)
 
@@ -2326,8 +2617,25 @@ def exponential_tp(x):
 
 #---------------------------------------- logarithm -----------------------------------------------
 def logarithm_imp(x, base=None):
-    """ 
+    """
+    Take the logarithm, optionally specify the base.
     base = None is synonymous with base = 2.718281828459045...
+   
+    ---- Examples ----
+    >>> 10 | logarithm         # => 2.302585092994046
+    >>> 100 | logarithm[10]    # => 2
+
+    ---- Signature ----
+    (Float, Float) -> Float
+
+    ---- Tags ----
+    used for: maths    
+    related zefop: exponential
+    related zefop: power
+    related zefop: multiply
+    """
+    """ 
+    
     """
     import math
     return (math.log(x) if base is None else math.log(x)/math.log(base))
@@ -2387,9 +2695,27 @@ def min_by_tp(v_tp, min_by_function_tp):
     
 #---------------------------------------- equals -----------------------------------------------
 def equals_imp(a, b):
-    return a == b
-    
-    
+    """
+    Binary operator equivalent to calling "==", but can be chained.
+   
+    ---- Examples ----
+    >>> 42 | equals[42]     # => True
+    >>> 42 | Not[equals][41]     # => True
+    >>> equals('a', 'a')     # => True
+
+    ---- Signature ----
+    (T, T) -> Bool
+
+    ---- Tags ----
+    used for: logic
+    
+    related zefop: Not
+    related zefop: greater_than
+    related zefop: less_than
+    """
+    return a == b
+    
+    
 def equals_tp(a, b):
     return VT.Bool
 
@@ -2397,6 +2723,24 @@ def equals_tp(a, b):
     
 #---------------------------------------- greater_than -----------------------------------------------
 def greater_than_imp(a, b):
+    """
+    Binary operator equivalent to calling ">", but can be chained.
+   
+    ---- Examples ----
+    >>> 42 | greater_than[41]     # => True
+    >>> 42 | greater_than[42]     # => False
+    
+    ---- Signature ----
+    (T, T) -> Bool
+
+    ---- Tags ----
+    used for: logic    
+    used for: maths    
+    related zefop: Not
+    related zefop: equals
+    related zefop: less_than
+    related zefop: greater_than_or_equal
+    """
     return a > b
     
     
@@ -2407,6 +2751,24 @@ def greater_than_tp(a, b):
     
 #---------------------------------------- less_than -----------------------------------------------
 def less_than_imp(a, b):
+    """
+    Binary operator equivalent to calling "<", but can be chained.
+   
+    ---- Examples ----
+    >>> 41 | less_than[42]     # => True
+    >>> 42 | less_than[42]     # => False
+    
+    ---- Signature ----
+    (T, T) -> Bool
+
+    ---- Tags ----
+    used for: logic    
+    used for: maths    
+    related zefop: Not
+    related zefop: equals
+    related zefop: greater_than
+    related zefop: less_than_or_equal
+    """
     return a < b
     
     
@@ -2415,8 +2777,26 @@ def less_than_tp(a, b):
 
 
     
-#---------------------------------------- larger_or_equal_than -----------------------------------------------
+#---------------------------------------- greater_than_or_equal -----------------------------------------------
 def greater_than_or_equal_imp(a, b):
+    """
+    Binary operator equivalent to calling ">=", but can be chained.
+   
+    ---- Examples ----
+    >>> 43 | greater_than_or_equal[42]     # => True
+    >>> 42 | greater_than_or_equal[42]     # => True
+    
+    ---- Signature ----
+    (T, T) -> Bool
+
+    ---- Tags ----
+    used for: logic    
+    used for: maths    
+    related zefop: Not
+    related zefop: equals
+    related zefop: greater_than
+    related zefop: less_than_or_equal
+    """
     return a >= b
     
 
@@ -2425,8 +2805,26 @@ def greater_than_or_equal_to(a, b):
 
 
     
-#---------------------------------------- less_or_equal_than -----------------------------------------------
+#---------------------------------------- less_than_or_equal -----------------------------------------------
 def less_than_or_equal_imp(a, b):
+    """
+    Binary operator equivalent to calling ">=", but can be chained.
+   
+    ---- Examples ----
+    >>> 41 | less_than_or_equal[42]     # => True
+    >>> 42 | less_than_or_equal[42]     # => True
+    
+    ---- Signature ----
+    (T, T) -> Bool
+
+    ---- Tags ----
+    used for: logic    
+    used for: maths    
+    related zefop: Not
+    related zefop: equals
+    related zefop: greater_than_or_equal
+    related zefop: less_than
+    """
     return a <= b
 
 
@@ -2438,8 +2836,25 @@ def less_than_or_equal_to(a, b):
 #---------------------------------------- Not -----------------------------------------------
 def not_imp(x, pred_fct=lambda x: x):
     """ 
-    Takes a single predicate function and returns the negated predicate function.    
-    """    
+    A logic combinator: takes a single predicate function 
+    (with any signature) and returns the negated predicate 
+    function with the same signature.
+    
+    ---- Examples ----
+    >>> 41 | Not[equals][42]     # => True
+    
+    ---- Signature ----
+    (T, T->Bool) -> Bool
+
+    ---- Tags ----
+    used for: logic    
+    used for: function composition
+    operates on: predicate function
+    related zefop: Not
+    related zefop: Or
+    related zefop: And
+    related zefop: xor
+    """
     return not pred_fct(x)
 
 
@@ -2474,7 +2889,8 @@ def and_imp(x, *args):
 
     ---- Tags ----
     used for: logic
-    used for: predicated
+    used for: predicate
+    operates on: predicate function
     related zefop: Or
     related zefop: Not
     related: Intersection
@@ -2530,7 +2946,8 @@ def or_imp(x, *args):
 
     ---- Tags ----
     used for: logic
-    used for: predicated
+    used for: predicate
+    operates on: predicate function
     related zefop: Or
     related zefop: Not
     related: Intersection
@@ -2564,6 +2981,26 @@ def or_tp(x, *args):
    
 #---------------------------------------- xor -----------------------------------------------
 def xor_imp(x, *args):
+    """ 
+    A logic combinator: takes two predicate functions
+    (with the same signature) and returns one predicate 
+    function with the same signature.
+    
+    ---- Examples ----
+    >>> 41 | xor[p1][p2]
+    
+    ---- Signature ----
+    (T, T->Bool, T->Bool) -> Bool
+
+    ---- Tags ----
+    used for: logic    
+    used for: function composition
+    operates on: predicate function
+    related zefop: Not
+    related zefop: Or
+    related zefop: And
+    related zefop: xor
+    """
     assert len(args) == 2
     f1, f2 = args
     res1, res2 = (f1(x), f2(x))
@@ -2578,33 +3015,6 @@ def xor_tp(x, *args):
        
    
 
-#---------------------------------------- drop -----------------------------------------------
-def drop_imp(v, n: int):
-    """
-    Drop the first n elements of the sequence
-    """
-    print("Deprecation: 😞😞😞😞😞 ops.drop is deprecated. Use 'skip'  instead 😞😞😞😞😞")
-    if isinstance(v, tuple) or isinstance(v, list) or isinstance(v, str):
-        return itertools.islice(v, n, None)  # None is 'inf' for the upper limit
-    # if isinstance(v, ZefRefs) or isinstance(v, EZefRefs):
-    #     return zefops.drop(v, n)
-    if n>=0:
-        it = iter(v)
-        for _ in range(n):
-            next(it)
-        def gen():
-            yield from it
-        return gen()    
-    # n<0
-    else:
-        cached = tuple(v)
-        return cached[:n]
-    
-
-def drop_tp(op, curr_type):
-    return curr_type
-
-
 
 #---------------------------------------- skip -----------------------------------------------
 def skip_imp(v, n: int):
@@ -3102,7 +3512,7 @@ def If_imp(x, pred, true_case_func, false_case_func):
     forwarded to the relevant case function.
 
     ---- Examples ----
-    >>> Evens = Is[modulus[2] | equals[0]]
+    >>> Evens = Is[modulo[2] | equals[0]]
     >>> 4 | If[ Evens ][add[1]][add[2]]            # => 5
 
     ---- Signature ----
@@ -3111,14 +3521,16 @@ def If_imp(x, pred, true_case_func, false_case_func):
     ---- Tags ----
     - used for: control flow
     - used for: logic
-    - related zefop: if_then_else
     - related zefop: group_by
     - related zefop: match
-    - related zefop: match_apply
     - related zefop: filter
     """
     try:
-        case = pred(x)
+        if isinstance(pred, ValueType_):
+            case = is_a(x, pred)
+        else:
+            # if not a VT: assume it is callable            
+            case = pred(x)
     except Exception as e:            
         raise RuntimeError(f'\nError within `If` zefop evaluating predicate function: `{pred}` for value  `{x}`: {e}')
     try:
@@ -3127,62 +3539,6 @@ def If_imp(x, pred, true_case_func, false_case_func):
         raise RuntimeError(f'\nError within `If` zefop evaluating the apply function for value  `{x}`: {e}')
 
 
-# --------------------------------------- if_then_else ------------------------------------------------
-def if_then_else_apply_imp(x, pred, true_case_func, false_case_func):
-    """
-    Dispatches to one of two provided functions based on the boolean 
-    result of the predicate function, given the input value.
-    The input value into the zefop is used by the predicate and 
-    forwarded to the relevant case function.
-
-    ---- Examples ----
-    >>> add_one, add_two = add[1], add[2]
-    >>> 4 | if_then_else[is_even][add_one][add_two]         # => 5
-
-    ---- Signature ----
-    ((T->Bool), (T->T1), (T->T2)) -> Union[T1, T2]
-
-    ---- Tags ----
-    - used for: control flow
-    - used for: logic
-    - related zefop: if_then_else
-    - related zefop: group_by
-    - related zefop: match
-    - related zefop: match_apply
-    - related zefop: filter
-    """
-    return true_case_func(x) if pred(x) else false_case_func(x)
-
-
-def if_then_else_apply_tp(x, pred, true_case_func, false_case_func):
-    return VT.Any
-
-
-def if_then_else_imp(x, pred, value1, value2):
-    """
-    Unlike the apply version of this function. If pred evaluates
-    to true, value1 is returned otherwise value2.
-
-    ---- Examples ----
-    >>> 4 | if_then_else[is_even]["even"]["odd"]       # => "even"
-
-    ---- Signature ----
-    (Any, (T->Bool), (Any), (Any)) -> Any
-
-    ---- Tags ----
-    - used for: control flow
-    - used for: logic
-    - related zefop: if_then_else_apply
-    - related zefop: group_by
-    - related zefop: match
-    - related zefop: match_apply
-    - related zefop: filter
-    """
-    return value1 if pred(x) else value2
-
-
-def if_then_else_tp(x, pred, true_case_func, false_case_func):
-    return VT.Any
 
 
 # ------------------------------------------ attempt ---------------------------------------------
@@ -3238,6 +3594,10 @@ def bypass_imp(x, bypass_type, fct):
 
     ---- Signature ----
     (List[T, T2], Type(T2), (T -> T3)) -> List[Union[T2, T3]]
+
+    ---- Tags ----
+    used for: control flow
+    operates on: ZefOp
     """
     type_tup = bypass_type if isinstance(bypass_type, tuple) else (bypass_type,)
     return if_then_else(
@@ -3276,12 +3636,14 @@ def pattern_imp(x, p):
     (Dict[T1, T2], Dict[Union[T1, _Any], Union[T2, _Any]]) -> Bool
 
     ---- Tags ----
-    - operates on: Dict, List
+    - operates on: List
+    - operates on: Dict
     - used for: control flow
     - related zefop: match
     - related zefop: match_apply
     - related zefop: distinct_by
     """
+    print(f"pattern will be retired: use `Pattern` and if a predicate is required `is_a[Pattern[...]]` instead. 🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱 ")
     from zef.ops import _any
     class Sentinel:
         pass
@@ -3343,20 +3705,28 @@ def distinct_imp(v):
     LazyValue[List[T]] -> LazyValue[List[T]]
 
     ---- Tags ----
-    - operates on: List, Stream
+    - operates on: Stream
+    - operates on: List
     - used for: list manipulation
     - related zefop: is_distinct
     - related zefop: distinct_by
     """
     observed = set()
+    unhashable = []
     it = iter(v)
+    from typing import Hashable
     def wrapper():
         try:
             while True:
                 el = next(it)
-                if el not in observed:
-                    observed.add(el)
-                    yield el
+                if isinstance(el, Hashable):
+                    if el not in observed:
+                        observed.add(el)
+                        yield el
+                else:
+                    if el not in unhashable:
+                        unhashable.append(el)
+                        yield el
         except StopIteration:
             return
 
@@ -3387,7 +3757,8 @@ def distinct_by_imp(v, fct):
     LazyValue[(List[T], (T->Any))] -> LazyValue[List[T]]
 
     ---- Tags ----
-    - operates on: List, Stream
+    - operates on: List
+    - operates on: Stream
     - used for: list manipulation
     - related zefop: is_distinct_by
     - related zefop: distinct
@@ -3434,7 +3805,8 @@ def is_distinct_imp(v):
     LazyValue[List[T]] -> Bool
 
     ---- Tags ----
-    - operates on: List, String
+    - operates on: String
+    - operates on: List
     - used for: set theory
     - related zefop: is_distinct_by
     - related zefop: distinct
@@ -3468,7 +3840,8 @@ def is_distinct_by_imp(v, fn):
     Stream[T], Callable -> Bool
 
     ---- Tags ----
-    - operates on: List, String
+    - operates on: String
+    - operates on: List
     - used for: set theory
     - related zefop: distinct_by
     - related zefop: is_distinct
@@ -3500,8 +3873,10 @@ def replace_imp(collection, old_val, new_val):
     String, Char, Char -> String
     
     ---- Tags ----
-    - operates on: List, String
+    - operates on: String
+    - operates on: List
     - used for: list manipulation
+    - used for: string manipulation
     - related zefop: replace_at
     - related zefop: insert_in
     - related zefop: insert
@@ -3596,7 +3971,6 @@ def slice_imp(v, start_end: tuple):
     - related zefop: last
     - related zefop: nth    
     """
-    print(type(v))
     from typing import Generator
     import itertools
     assert isinstance(start_end, tuple)
@@ -3638,6 +4012,16 @@ def split_imp(collection, val):
 
     ---- Signature ----
     (List[T], T) -> List[List[T]]
+
+    ---- Tags ----
+    - operates on: List
+    - operates on: String
+    - used for: list manipulation
+    - used for: string manipulation
+    - related zefop: split_left
+    - related zefop: split_right
+    - related zefop: split_if
+    - related zefop: trim
     """
     if isinstance(collection, str):
         return collection.split(val)
@@ -3661,6 +4045,93 @@ def split_tp(*args):
     return VT.Any
 
 
+# ---------------------------------------- split_left -----------------------------------------------
+def split_left_imp(v, val):
+    """ 
+    Split a List into a List[List] based on the occurrence of val.
+    In contrast to `split`, it adds the element to split on to the
+    following sublist - i.e. it splits on the left side.
+    
+    ---- Examples ----
+    >>> 'abcdeabfb' | split_left['b']            # => ['a', 'bcdea', 'bf', 'b']
+    >>> [0,1,6,2,3,4,2,] | split_left[2]         # => [[0, 1, 6], [2, 3, 4], [2]]    
+
+    ---- Signature ----
+    (List[T], T) -> List[List[T]]
+
+    ---- Tags ----
+    - operates on: List
+    - operates on: String
+    - used for: list manipulation
+    - used for: string manipulation
+    - related zefop: split
+    - related zefop: split_right
+    - related zefop: split_if
+    - related zefop: trim
+    """
+    if isinstance(v, str):
+        return split_left_imp(iter(v), val) | map[concat] | collect
+
+    def wrapper():
+        it = iter(v)
+        try:
+            next_val = next(it)
+            while True:
+                s = []
+                s.append(next_val)
+                next_val = next(it)
+                while next_val != val:                
+                    s.append(next_val)
+                    next_val = next(it)                    
+                yield s
+        except StopIteration:
+            if s!=[]: yield s
+            return
+    return wrapper()
+
+
+# ---------------------------------------- split_right -----------------------------------------------
+def split_right_imp(v, val):
+    """ 
+    Split a List into a List[List] based on the occurrence of val.
+    In contrast to `split`, it adds the element to split on to the
+    left sublist - i.e. it splits on the right side.
+    
+    ---- Examples ----
+    >>> 'abcdeabfb' | split_right['b']            # => ['ab', 'cdeab', 'fb']
+    >>> [0,1,6,2,3,4,2,] | split_right[2]         # => [[0, 1, 6, 2], [3, 4, 2]]    
+
+    ---- Signature ----
+    (List[T], T) -> List[List[T]]
+
+    ---- Tags ----
+    - operates on: List
+    - operates on: String
+    - used for: list manipulation
+    - used for: string manipulation
+    - related zefop: split
+    - related zefop: split_left
+    - related zefop: split_if
+    - related zefop: trim
+    """
+    if isinstance(v, str):
+        return split_right_imp(iter(v), val) | map[concat] | collect
+
+    def wrapper():
+        it = iter(v)
+        try:
+            while True:
+                s = []
+                next_val = next(it)
+                s.append(next_val)
+                while next_val != val:                
+                    next_val = next(it)                    
+                    s.append(next_val)
+                yield s
+        except StopIteration:
+            if s!=[]: yield s
+            return
+    return wrapper()
 
 
 # ------------------------------------------ split_if ---------------------------------------------
@@ -3679,12 +4150,14 @@ def split_if_imp(v, split_function):
     (List[T], T->Bool) -> List[List[T]]
 
     ---- Tags ----
-    - operates on: List, String
+    - operates on: List
+    - operates on: String
     - used for: list manipulation
     - used for: string manipulation
     - related zefop: split
     - related zefop: concat
     - related zefop: trim
+    - used for: predicate
     """
     if isinstance(v, str):
         return v | func[tuple] | split_if[split_function] | map[join] | collect
@@ -3707,41 +4180,6 @@ def wrapper():
 
 
 
-# ------------------------------------------ tx ---------------------------------------------
-
-def tx_imp(*args):
-    """
-    let zr: ZefRef  ezr: EZefRef
-    zr | tx[instantiated]       # keeps ref. frame of zr, returns a ZefRef: 
-    zr | tx[terminated]
-    """
-    print(f"😭😭😭😭😭😭😭😭 Deprecation warning: the 'tx' operator will be deprecated. Its usage was too broad and mixed up concepts. Use 'to_graph_slice' and 'to_tx' instead")
-    # print(f">>>>> args[0]={args[0]}")
-    # print(f">>>>> args[1]={args[1]}")
-    # print(f">>>>> {args[1]== lazy_zefops.instantiated}")
-    x = args[0]
-    if len(args) == 1:          
-        if isinstance(x, GraphSlice):
-            zz = x.tx
-            return ZefRef(zz, zz)
-        if isinstance(x, ZefRef):
-            return pyzefops.tx(x)
-    
-    if len(args) == 2:        
-        if type(args[1]) == ZefOp:
-            if args[1].el_ops[0][0] == RT.Instantiated:                                
-                return pyzefops.instantiation_tx(x)
-            if args[1].el_ops[0][0] == RT.Terminated:                                
-                return pyzefops.termination_tx(x)
-
-    raise Exception("Unknown type for tx op type(args[0])={type(args[0])}")
-
-
-def tx_tp(op, curr_type):
-    return curr_type
-
-
-
 # --------------------------------------- now ------------------------------------------------
 
 def now_implementation(*args):
@@ -3825,9 +4263,9 @@ def now_type_info(*args_tp):
 
 # ---------------------------------------- time_slice -----------------------------------------------
 
-def time_slice_implementation(first_arg, *curried_args):
+def time_slice_implementation(gs: GraphSlice, *curried_args):
     """ 
-    Returns the time slice as an Int for GraphSlice.
+    Returns the time slice as an special `TimeSlice` object for a given  GraphSlice.
     
     Note: we may introduce z_tx | time_slice if we can find a convincing use case.
     We're leaving it out for now, since this builds on the prior confusion
@@ -3839,23 +4277,49 @@ def time_slice_implementation(first_arg, *curried_args):
     >>> my_graph_slice | time_slice
 
     ---- Signature ----
-    GraphSlice -> Int
+    GraphSlice -> TimeSlice
     """
-    if isinstance(first_arg, GraphSlice):
-        return pyzefops.time_slice(first_arg.tx)
-
-    print(f"😞😞😞😞😞  The use of the 'time_slice' on arbitrary ZefRefs is deprecated: use 'frame | time_slice' or 'to_graph_slice | time_slice' instead 😞😞😞😞😞")  
+    print(f"😔😔😔😔😔😔 RThis zefop will be deprecated. The TimeSlice data structure \
+         (which it returns) has been conceptually replaced with 'GraphSlice'.")
+    if isinstance(gs, GraphSlice):
+        return pyzefops.time_slice(gs.tx)
 
-    # Old usage:
-    if not is_a(first_arg, BT.TX_EVENT_NODE):
-        print(f"😞😞😞😞😞  The use of the 'time_slice' operator with type(first_arg)={type(first_arg)} is deprecated: 😞😞😞😞😞")    
-    return (pyzefops.time_slice)(first_arg, *curried_args).value
+    raise TypeError(f"The 'time_slice' ZefOp can only be used on GraphSlices. Use 'frame | time_slice' or 'to_graph_slice | time_slice' ")  
 
 
 def time_slice_type_info(op, curr_type):
     return VT.TimeSlice
 
 
+# ---------------------------------------- graph_slice_index -----------------------------------------------
+
+def graph_slice_index_imp(gs: GraphSlice, *curried_args):
+    """ 
+    Returns the index of a GraphSlice as an Int. 
+    
+    Historical note: the GraphSlice data structure itself replaces 
+    what was previously the TimeSlice.
+
+    Historical note 2: There was initial confusion in conflating the 
+    transactions themselves with time slices / graph slices.
+    These are distinct concepts: TXs are sets of changes, 
+    i.e. events: "what happened?"
+    
+    GraphSlices are states: "What is?" They are also known as 
+    "reference frames"
+    
+    ---- Examples ----
+    >>> my_graph_slice | graph_slice_index
+
+    ---- Signature ----
+    (GraphSlice, ) -> Int
+    """
+    if isinstance(gs, GraphSlice):
+        return int(pyzefops.time_slice(gs.tx))
+
+    raise TypeError(f"The 'graph_slice_index' ZefOp can only be used on GraphSlices. You may want to use the 'frame' or 'to_graph_slice' zefops")  
+
+
 
 # ---------------------------------------- next_tx -----------------------------------------------
 def next_tx_imp(z_tx):
@@ -3866,14 +4330,19 @@ def next_tx_imp(z_tx):
     in that reference frame. Nil is returned in that case.
     
     For Both ZeFfRef/EZefRef, Nil is returned when called
-    on the very lastest TX.
+    on the very latest TX.
 
     ---- Examples ----
     z2_tx = z_tx | next_tx | collect
 
     ---- Signature ----
     ZefRef -> Union[ZefRef, Nil]
-    EZefRef -> Union[EZefRef, Nil]    
+    EZefRef -> Union[EZefRef, Nil]
+
+    ---- Tags ----
+    - used for: time traversal
+    - related zefop: previous_tx
+    - related zefop: time_travel
     """
     def next_tx_ezr(zz):
         try:
@@ -3907,11 +4376,17 @@ def previous_tx_imp(z_tx):
     If one tries to go back from the very first tx, nil is 
     returned.    
     
+    ---- Examples ----
+    z2_tx = z_tx | previous_tx | collect
+
     ---- Signature ----
     ZefRef -> Union[ZefRef, Nil]
     EZefRef -> Union[EZefRef, Nil]
     
-    z2_tx = z_tx | previous_tx | collect
+    ---- Tags ----
+    - used for: time traversal
+    - related zefop: previous_tx
+    - related zefop: time_travel
     """
     def previous_tx_ezr(zz):
         try:
@@ -3937,6 +4412,75 @@ def previous_tx_tp(z_tp):
     return None
 
 
+# ----------------------------------------- preceding_events --------------------------------------------
+
+def preceding_events_imp(x, filter_on=None):
+    """ 
+    Given a TX as a (E)ZefRef, return all events that occurred in that TX.
+    Given a ZefRef to a RAE, return all the events that happend on the RAE.
+
+    - filter_on allows for filtering on the type of the events; by default it is none which
+    returns all events.
+
+    ---- Examples ----
+    >>> my_graph_slice | events                  => [instantiated[z1], terminated[z3], assigned[z8][41][42] ]
+    >>> z_rae | events                          => [instantiated[z1], assigned[z8][41][42], terminated[z3]]
+    >>> z_rae | events[Instantiated]            => [instantiated[z1]]
+    >>> z_rae | events[Instantiated | Assigned] => [instantiated[z1], assigned[z8][41][42]]
+
+    ---- Signature ----
+    Union[ZefRef[TX], EZefRef[TX]]  ->  List[ZefOp[Union[Instantiated[ZefRef], Terminated[ZefRef], ValueAssigned[ZefRef, T]]]]
+    """
+    from zef.pyzef import zefops as pyzefops
+    # Note: we can't use the python to_frame here as that calls into us.
+    
+    if isinstance(x, GraphSlice):
+        return 'TODO!!!!!!!!!!!!!!!'
+
+    if BT(x) == BT.TX_EVENT_NODE:
+        raise TypeError(f"`preceding_events` can only be called on RAEs and GraphSlices and lists all relevant events form the past. It was called on a TX. You may be looking for the `events` operator, which lists all events that happened in a TX.")
+        
+    if BT(x) not in [
+            BT.ENTITY_NODE,
+            BT.RELATION_EDGE,
+            BT.ATOMIC_ENTITY_NODE,
+            ]:
+        raise TypeError(f"`preceding_events` can only be called on RAEs and GraphSlices and lists all relevant events form the past.")
+
+    if internals.is_delegate(x):
+        ezr = to_ezefref(x)
+        to_del = ezr | in_rel[BT.TO_DELEGATE_EDGE] | collect
+        insts = to_del | Ins[BT.DELEGATE_INSTANTIATION_EDGE]
+        retirements = to_del | Ins[BT.DELEGATE_RETIREMENT_EDGE]
+        if type(ezr) == ZefRef:
+            gs = frame(ezr)
+            insts = insts | filter[graph_slice_index | less_than_or_equal[graph_slice_index(gs)]]
+            retirements = insts | filter[graph_slice_index | less_than_or_equal[graph_slice_index(gs)]]
+
+        insts = insts | map[lambda tx: instantiated[pyzefops.to_frame(ezr, tx, True)]] | collect
+        retirements = retirements | map[lambda tx: terminated[pyzefops.to_frame(ezr, tx, True)]] | collect
+        full_list = insts+retirements
+    else:
+        zr = x
+
+        def make_val_as_from_tx(tx):
+            aet_at_frame = pyzefops.to_frame(zr, tx)
+            try:
+                prev_tx  = tx | previous_tx | collect                                   # Will fail if tx is already first TX
+                prev_val = pyzefops.to_frame(zr, prev_tx) | value | collect     # Will fail if aet didn't exist at prev_tx
+            except:
+                prev_val = None
+            return assigned[aet_at_frame][prev_val][value(aet_at_frame)]
+
+        inst        =  [pyzefops.instantiation_tx(zr)]   | map[lambda tx: instantiated[pyzefops.to_frame(zr, tx) ]] | collect
+        val_assigns =  pyzefops.value_assignment_txs(zr) | map[make_val_as_from_tx] | collect
+        # TODO termination_tx returns even if zr is a zefref with a timeslice where it wasn't terminated yet
+        termination =  [pyzefops.termination_tx(zr)]     | filter[lambda b: BT(b) != BT.ROOT_NODE] | map[lambda tx: terminated[pyzefops.to_frame(zr, tx, True)]] | collect
+        full_list = inst + val_assigns + termination 
+
+    if filter_on: return full_list | filter[lambda z: is_a_implementation(z, filter_on)] | collect
+    return full_list
+
 
 
 
@@ -3951,16 +4495,22 @@ def events_imp(z_tx_or_rae, filter_on=None):
     returns all events.
 
     ---- Examples ----
-    >>> z_tx  | events                          => [instantiated[z1], terminated[z3], value_assigned[z8][41][42] ]
-    >>> z_rae | events                          => [instantiated[z1], value_assigned[z8][41][42], terminated[z3]]
-    >>> z_rae | events[Instantiated]            => [instantiated[z1]]
-    >>> z_rae | events[Instantiated | Assigned] => [instantiated[z1], value_assigned[z8][41][42]]
+    >>> z_tx  | events                          => [instantiated[z1], terminated[z3], assigned[z8][41][42] ]
+
 
     ---- Signature ----
-    Union[ZefRef[TX], EZefRef[TX]]  ->  List[ZefOp[Union[Instantiated[ZefRef], Terminated[ZefRef], ValueAssigned[ZefRef, T]]]]
+    ZefRef[TX] | EZefRef[TX]  ->  List[ZefOp[Union[Instantiated[ZefRef], Terminated[ZefRef], ValueAssigned[ZefRef, Any, Any]]]]
+
+    ---- Tags ----
+    - related zefop: preceding_events
+    - related zefop: instantiation_txs
+    - related zefop: termination_txs
+    - related zefop: assignment_txs
+
     """
     from zef.pyzef import zefops as pyzefops
     # Note: we can't use the python to_frame here as that calls into us.
+    
 
     if internals.is_delegate(z_tx_or_rae):
         ezr = to_ezefref(z_tx_or_rae)
@@ -3969,8 +4519,8 @@ def events_imp(z_tx_or_rae, filter_on=None):
         retirements = to_del | Ins[BT.DELEGATE_RETIREMENT_EDGE]
         if type(ezr) == ZefRef:
             gs = frame(ezr)
-            insts = insts | filter[time_slice | less_than_or_equal[time_slice(gs)]]
-            retirements = insts | filter[time_slice | less_than_or_equal[time_slice(gs)]]
+            insts = insts | filter[graph_slice_index | less_than_or_equal[graph_slice_index(gs)]]
+            retirements = insts | filter[graph_slice_index | less_than_or_equal[graph_slice_index(gs)]]
 
         insts = insts | map[lambda tx: instantiated[pyzefops.to_frame(ezr, tx, True)]] | collect
         retirements = retirements | map[lambda tx: terminated[pyzefops.to_frame(ezr, tx, True)]] | collect
@@ -3987,13 +4537,16 @@ def make_val_as_for_aet(aet):
                 prev_val = pyzefops.to_frame(aet, prev_tx) | value | collect   # Will fail if aet didn't exist at prev_tx
             except:
                 prev_val = None
-            return value_assigned[aet_at_frame][prev_val][value(aet_at_frame)]
+            return assigned[aet_at_frame][prev_val][value(aet_at_frame)]
 
         insts        = zr | pyzefops.instantiated   | map[lambda zz: instantiated[pyzefops.to_frame(zz, gs.tx) ]] | collect
         val_assigns  = zr | pyzefops.value_assigned | map[make_val_as_for_aet] | collect
         terminations = zr | pyzefops.terminated     | map[lambda zz: terminated[pyzefops.to_frame(zz, gs.tx, True) ]] | collect
         full_list = insts+val_assigns+terminations
     else:
+        print(f"🥱🥱🥱🥱🥱🥱 Change: The `events` ZefOp can only be used on (E)ZefRef to TXs. To look at the past of any RAE from a given frame, use `preceding_events`. ")
+        raise Exception()
+        # TODO: retire this block
         zr = z_tx_or_rae
 
         def make_val_as_from_tx(tx):
@@ -4086,22 +4639,7 @@ def in_frame_imp(z, *args):
     if is_same_g or origin_uid(zz).graph_uid == uid(g_frame):
         z_obj = to_ezefref(z) if is_same_g else g_frame[origin_uid(zz)]
         # exit early if we are looking in a frame prior to the objects instantiation: this is not even allowed when allow_tombstone=True
-        if BT(z_obj) == BT.TX_EVENT_NODE:
-            # TODO: This should not be necessary in the future, as
-            # events[Instantiated] should cover this and the following ROOT_NODE
-            # case.
-            instantiation_ts = pyzefops.time_slice(z_obj)
-        elif BT(z_obj) == BT.ROOT_NODE:
-            # TODO: It should also be possible to pass this to
-            # events[Instantiated], as then the delegates of the root node can
-            # also be handled seamlessly
-            instantiation_ts = TimeSlice(0)
-        else:
-            # Note that 'first' is used here instead of 'single', as delegates
-            # may have been brought back to life... but we only care about
-            # casuality here, which corresponds to the very first instantiation.
-            instantiation_ts = z_obj | events[Instantiated] | first | absorbed | single | frame | time_slice | collect
-        if (time_slice(target_frame)) < instantiation_ts:
+        if z_obj | Not[aware_of[target_frame]] | collect:
             raise RuntimeError(f"Causality error: you cannot point to an object from a frame prior to its existence / the first time that frame learned about it.")            
         if not tombstone_allowed:
             if not exists_at(z_obj, target_frame):
@@ -4241,20 +4779,19 @@ def time_travel_imp(x, *args):
     ---- Examples ----
     >>> #       ---- relative time travel ----
     >>> zr | time_travel[-3]                    # how many time slices to move
-    >>> zr | time_travel[-3.5*units.seconds]  
-    >>> my_graph_slice | time_travel[-3]
-    >>> 
-    >>> #       ---- absolute time travel ----
+    ... zr | time_travel[-3.5*units.seconds]  
+    ... my_graph_slice | time_travel[-3]
+     
+    >>> #       ---- time travel to fixed time ----
     >>> t1 = Time('October 20 2020 14:00 (+0100)')
-    >>> g | time_travel[t1]
-    >>> gs | time_travel[t1]
-    >>> ezr | time_travel[t1]
-    >>> zr | time_travel[t1]
-    >>>
-    >>> ezr | time_travel[allow_tombstone][t1]
-    >>> zr | time_travel[allow_tombstone][-3]
-    >>> zr | time_travel[allow_tombstone][-3.5*units.seconds]
-    >>> zr | time_travel[allow_tombstone][t1]
+    ... g | time_travel[t1]
+    ... gs | time_travel[t1]
+    ... ezr | time_travel[t1]
+    ... zr | time_travel[t1]
+    ... ezr | time_travel[allow_tombstone][t1]
+    ... zr | time_travel[allow_tombstone][-3]
+    ... zr | time_travel[allow_tombstone][-3.5*units.seconds]
+    ... zr | time_travel[allow_tombstone][t1]
 
     ---- Signature ----
     (ZefRef, Int)                           -> Union[ZefRef, Nil]           # nil or error? What is the process / criterion?
@@ -4270,6 +4807,9 @@ def time_travel_imp(x, *args):
     ---- Tags ----
     - used for: time traversal
     - related zefop: time_slice    
+    - related zefop: next_tx
+    - related zefop: previous_tx
+    - related zefop: time
     """
     c = collect 
 
@@ -4419,16 +4959,17 @@ def origin_rae_tp(x):
 
 
 def fill_or_attach_implementation(z, rt, val):
-    # from ...deprecated import zefops
-    # return curry_args_in_zefop(zefops.fill_or_attach, args[0], args[1:])
-    if has_out(z, rt):
-        return z | Out[rt] | assign_value[val] | collect
-    else:
-        return (z, rt, val)
+    return LazyValue(z) | fill_or_attach[rt][val]
 
 def fill_or_attach_type_info(op, curr_type):
     return curr_type
 
+def set_field_implementation(z, rt, val):
+    return LazyValue(z) | set_field[rt][val]
+
+def set_field_type_info(op, curr_type):
+    return curr_type
+
 def assert_implementation(z, predicate=None, message=None):
     if predicate is None:
         success = z
@@ -4456,7 +4997,6 @@ def assert_type_info(op, curr_type):
     
 def run_effect_implementation(eff):
     from ..fx import _effect_handlers
-    from ..fx.fx_types import _Effect_Class
     
     # if we create the effect with short hand notation, e.g. (ET.Dog, ET.Person | g | run)
     # we want to directly unpack the instances in the same structure as the types that went in
@@ -4464,10 +5004,9 @@ def run_effect_implementation(eff):
     # as a key in the receipt
 
     
-    if not isinstance(eff, _Effect_Class):
-        raise TypeError(f"run(x) called with invalid type for x: {type(eff)}. You can only run an Effect.")
-    handler = _effect_handlers[eff.d['type'].d]
-    eff._been_run = True
+    if not isinstance(eff, dict):
+        raise TypeError(f"run(x) called with invalid type for x: {type(eff)}. You can only run a wish, which are represented as dictionaries.")
+    handler = _effect_handlers[eff['type'].d]
     return handler(eff)
 
 
@@ -4497,7 +5036,8 @@ def apply_functions_imp(x, fns):
 
     ---- Tags ----
     - used for: control flow
-    - operates on: ZefOps, Functions
+    - operates on: ZefOp
+    - operates on: Function
     - related zefop: func
     - related zefop: reverse_args
     - related zefop: map
@@ -4559,25 +5099,31 @@ def wrapper():
 
 
 
-   
+# -------------------------------- reduce -------------------------------------------------
+ 
 
 def reduce_implementation(iterable, fct, init):
     import functools
     return functools.reduce(fct, iterable, init)
 
 
-# def group_by_implementation(first_arg, *curried_args):
-#     return (ops.group_by)(first_arg, *curried_args)
 
+# -------------------------------- group_by -------------------------------------------------
 def group_by_implementation(iterable, key_fct, categories=None):
     """
     categories is optional and specifies additional keys/categories 
-    to create, even if there are no occurrences. Passed as a list 
-    [True, False]
+    to create, even if there are no occurrences. The categories can
+    be implicitly inferred from the data or explicitly specified 
+    (useful for dispatch and allowing for empty catergories).
+
+    ---- Examples ----
+    >>> range(10) | group_by[modulo[3]]   # => [(0, [0, 3, 6, 9]), (1, [1, 4, 7]), (2, [2, 5, 8])]
     
     ---- Tags ----
     - used for: control flow
-    - operates on: List    
+    - operates on: List
+    - operates on: Stream
+    - related zefop: group
     """        
     # map to list first in case the generator state changes
     from collections import defaultdict
@@ -4595,10 +5141,95 @@ def group_by_implementation(iterable, key_fct, categories=None):
 
 
 
+# -------------------------------- group -------------------------------------------------
+def group_imp(v, f=lambda x: x):
+    """
+    Examine successive elements in a List and return 
+    a lazy list of lists: the given function f maps 
+    the elements within each sublist map onto the 
+    same value, i.e. the splitting occurs where mapped
+    elements differ.
+
+    ---- Examples ----
+    >>> ['a','a','a','b','b','c','a', 'a'] | group      # => [('a', 'a', 'a'), ('b', 'b'), ('c',), ('a', 'a')]
+    >>> [2,4,6,1,3,2,5,7,9,10] | group[lambda x: x%2]   # => [(2, 4, 6), (1, 3), (2,), (5, 7, 9), (10,)]
+
+    ---- Signature ----
+    (List[T], (T)->T2) -> List[List[T]]
+    
+    ---- Tags ----
+    - operates on: List
+    - operates on: String
+    - used for: list manipulation
+    - used for: string manipulation
+    - related zefop: group_by
+    - related zefop: chunk
+    """
+    def wrapper():        
+        it = iter(v)
+        current_list = []
+        try:
+            last_val = next(it)
+            last_f_val = f(last_val)
+            while True:
+                current_list = [last_val]
+                while True:                    
+                    val = next(it)
+                    f_val = f(val)
+                    if f_val != last_f_val:
+                        yield tuple(current_list)
+                        last_val = val
+                        last_f_val = f_val
+                        break
+                    current_list.append(val)
+                    last_val = val
+                    last_f_val = f_val
+        except StopIteration:
+            if current_list != []:
+                yield tuple(current_list)
+            return
+
+    return wrapper()
+
+
+
+
+
+
+# -------------------------------- identity -------------------------------------------------
+
 def identity_implementation(x):
+    """
+    The identity function: always returns the input argument
+
+    ---- Examples ----
+    >>> 42 | identity     # => 42
+
+    ---- Tags ----
+    operates on: Any
+    used for: function composition
+    """
     return x
 
+
+# -------------------------------- length -------------------------------------------------
 def length_implementation(iterable):
+    """
+    Zef's version of the length function.
+    Similar to Python's `len` and sometimes
+    called `count` in other languages.
+
+    ---- Examples ----
+    >>> ['a', 'b', 'c'] | length     # => 3
+
+    ---- Signature ----
+    (List[T]) -> Int
+
+    ---- Tags ----
+    related zefop: count
+    operates on: List
+    operates on: Stream
+    """
     if hasattr(iterable, "__len__"):
         return len(iterable)
     else:
@@ -4607,11 +5238,15 @@ def length_implementation(iterable):
 
 
 
+# -------------------------------- nth -------------------------------------------------
 def nth_implementation(iterable, n):
     """
     Returns the nth element of an iterable or a stream.
-    Using negative 'n' takes from the back with 'nth[-1]' being the last element.
-    An Error value is returned if the index is out of bounds.
+    Using negative 'n' takes from the back with 'nth[-1]' 
+    being the last element. An Error value is returned if 
+    the index is out of bounds.
+    This is sometimes called 'get` in other languages, but
+    we want to distinguish it from key lookup.
     Note: this uses a zero-indexed convention.
 
     ---- Examples ----
@@ -4692,7 +5327,7 @@ def modulo_imp(m: int, n: int):
 
 
 #---------------------------------------- select_by_field -----------------------------------------------
-def filter_implementation(itr, pred):
+def filter_implementation(itr, pred_or_vt):
     """
     Filters an iterable or stream lazily.
 
@@ -4711,6 +5346,10 @@ def filter_implementation(itr, pred):
     - used for: control flow
     - operates on: List
     """
+    if isinstance(pred_or_vt, ValueType_): pred = is_a[pred_or_vt]
+    elif isinstance(pred_or_vt, set): pred = lambda x: x in pred_or_vt
+    else: pred = pred_or_vt
+    
     input_type = parse_input_type(type_spec(itr))
     if input_type == "tools":
         # As this is an intermediate, we return an explicit generator
@@ -4741,6 +5380,7 @@ def select_by_field_imp(zrs : Iterable[ZefRef], rt: RelationType, val):
     ---- Tags ----
     - operates on: Graph
     - related zefop: filter
+    - related zefop: value    
     """
     return pyzefops.select_by_field_impl(zrs, rt, val)
 
@@ -5118,29 +5758,16 @@ def in_rels_imp(z, rt=None, source_filter=None):
 
 
 
-
-
-
-def ins_implementation_old(first_arg, *curried_args):
-    print("Deprecation: 😞😞😞😞😞 ops.ins is deprecated. Use 'z1 < L[RT]'  instead of 'z1 | ins' 😞😞😞😞😞")
-    if isinstance(first_arg, FlatRef): return fr_ins_imp(first_arg)
-    return (pyzefops.ins)(first_arg, *curried_args)
-
-def outs_implementation_old(zr):
-    print("Deprecation: 😞😞😞😞😞 ops.outs is deprecated. Use 'z1 > L[RT]'  instead of 'z1 | outs' 😞😞😞😞😞")
-    if isinstance(zr, FlatRef): return fr_outs_imp(zr)
-    return pyzefops.outs(zr)
-
-def ins_and_outs_implementation_old(first_arg, *curried_args):
-    print("Deprecation: 😞😞😞😞😞 ops.ins_and_outs is deprecated. use 'z1 < L[RT]' and 'z1 > L[RT]' instead😞😞😞😞😞")    
-    if isinstance(first_arg, FlatRef): return fr_ins_and_outs_imp(first_arg)
-    return (pyzefops.ins_and_outs)(first_arg, *curried_args)
-
 def source_implementation(zr, *curried_args):
     if isinstance(zr, FlatRef):
         return fr_source_imp(zr)
     if isinstance(zr, Relation):
         return abstract_rae_from_rae_type_and_uid(zr.d["type"][0], zr.d["uids"][0])
+    if isinstance(zr, Delegate):
+        from ...pyzef.internals import DelegateRelationTriple
+        if not isinstance(zr.item, DelegateRelationTriple):
+            raise Exception(f"Can't take the source of a non-relation-triple Delegate ({zr})")
+        return Delegate(zr.order + zr.item.source.order, zr.item.source.item)
     if is_a(zr, ET):
         raise Exception(f"Can't take the source of an entity (have {zr}), only relations have sources/targets")
     return (pyzefops.source)(zr, *curried_args)
@@ -5150,6 +5777,11 @@ def target_implementation(zr):
         return fr_target_imp(zr)
     if isinstance(zr, Relation):
         return abstract_rae_from_rae_type_and_uid(zr.d["type"][2], zr.d["uids"][2])
+    if isinstance(zr, Delegate):
+        from ...pyzef.internals import DelegateRelationTriple
+        if not isinstance(zr.item, DelegateRelationTriple):
+            raise Exception(f"Can't take the target of a non-relation-triple Delegate ({zr})")
+        return Delegate(zr.order + zr.item.target.order, zr.item.target.item)
     if is_a(zr, ET):
         raise Exception(f"Can't take the target of an entity (have {zr}), only relations have sources/targets")
     return pyzefops.target(zr)
@@ -5191,15 +5823,13 @@ def time_implementation(x, *curried_args):
 
 
 def instantiation_tx_implementation(z):
-    return z | events[Instantiated] | single | absorbed | single | frame | to_tx | collect
+    return z | preceding_events[Instantiated] | single | absorbed | single | frame | to_tx | collect
 
 def termination_tx_implementation(z):
     root_node = Graph(z)[42] 
-    return z | events[Terminated] | attempt[single | absorbed | single | frame | to_tx][root_node] | collect
+    return z | preceding_events[Terminated] | attempt[single | absorbed | single | frame | to_tx][root_node] | collect
+
 
-def instances_implementation(*args):
-    print("Deprecation: 😞😞😞😞😞 instances is deprecated. Use 'f | all'  instead 😞😞😞😞😞")
-    return all(*args)
     
 def uid_implementation(arg):
     if isinstance(arg, str):
@@ -5219,9 +5849,69 @@ def base_uid_implementation(first_arg):
         return first_arg
     return base_uid(uid(first_arg))
 
-def exists_at_implementation(first_arg, frame):
+def exists_at_implementation(z, frame):
     assert isinstance(frame, GraphSlice)
-    return (pyzefops.exists_at)(first_arg, frame.tx)
+    return (pyzefops.exists_at)(z, frame.tx)
+
+def first_tx_for_low_level_blob(z):
+    # Delegate detection comes first, as several of the other things below can
+    # be delegates and would need their own logic.
+    if internals.is_delegate(z):
+        # Chronological order is mandatory so we find the first instantiation edge
+        return first_tx_for_low_level_blob(z | in_rel[BT.TO_DELEGATE_EDGE] | collect)
+    elif BT(z) in [BT.ENTITY_NODE,
+                   BT.RELATION_EDGE,
+                   BT.ATOMIC_ENTITY_NODE]:
+        # Chronological order is mandatory so we find the first instantiation edge
+        return first_tx_for_low_level_blob(z | in_rel[BT.RAE_INSTANCE_EDGE] | collect)
+    if BT(z) in [BT.DELEGATE_INSTANTIATION_EDGE,
+                   BT.DELEGATE_RETIREMENT_EDGE,
+                   BT.INSTANTIATION_EDGE,
+                   BT.TERMINATION_EDGE,
+                   BT.ATOMIC_VALUE_ASSIGNMENT_EDGE,
+                   BT.ASSIGN_TAG_NAME_EDGE]:
+        # Any of these are low-level blobs that only have a single event of "instantiation" and whose source is the tx
+        return source(z)
+    elif BT(z) in [BT.ORIGIN_RAE_EDGE,
+                   BT.ORIGIN_GRAPH_EDGE,
+                   BT.FOREIGN_ENTITY_NODE,
+                   BT.FOREIGN_RELATION_EDGE,
+                   BT.FOREIGN_ATOMIC_ENTITY_NODE,
+                   BT.FOREIGN_GRAPH_NODE]:
+        raise TypeError(f"Can't yet determine events for this kind of blob: {BT(z)}")
+                   
+    # Special cases where there is a TX but it's a bit harder to get to
+    elif BT(z) == BT.TX_EVENT_NODE:
+        return z
+    elif BT(z) == BT.NEXT_TX_EDGE:
+        return target(z)
+    elif BT(z) == BT.ROOT_NODE:
+        # This is a little of an odd choice
+        return z | Out[BT.NEXT_TX_EDGE] | collect
+    elif BT(z) == BT.RAE_INSTANCE_EDGE:
+        return z | Ins[BT.INSTANTIATION_EDGE] | first | collect
+    elif BT(z) == BT.TO_DELEGATE_EDGE:
+        # Need to get the first TX of all instantiation edges.
+        return z | Ins[BT.DELEGATE_INSTANTIATION_EDGE] | first | collect
+
+    raise Exception("Not a low level blob we can get a simple tx for.")
+
+
+def aware_of_implementation(z, frame):
+    """
+    Return whether the Graph is aware of the object `z` in the frame given. This
+    differs from exists_at in that a graph is still "aware" of a terminated
+    entity, and low-level blobs are also valid inputs to aware_of.
+    
+    ---- Signature ----
+    ((ZefRef | EZefRef), GraphSlice) -> Bool
+    """
+    z_tx = first_tx_for_low_level_blob(z)
+    return (z_tx
+            | to_graph_slice
+            | graph_slice_index
+            | less_than_or_equal[graph_slice_index(frame)]
+            | collect)
 
 def is_zefref_promotable_implementation(z):
     return pyzefops.is_zefref_promotable(z)
@@ -5343,9 +6033,44 @@ def is_matching(el, setof):
         return True
     
 
+    def rp_matching(x, rp):
+        triple = rp.d['absorbed'][0]
+        v = tuple(el == Z for el in triple)
+        try:
+            if v == (True, False, False):
+                return x | Out[triple[1]] | is_a[triple[2]] | collect
+
+            if v == (False, False, True):
+                return x | In[triple[1]] | is_a[triple[0]] | collect
+
+            if v == (False, True, False):
+                return is_a(source(x), triple[0]) and is_a(target(x), triple[2])
+                
+            if  v == (False, False, False):
+                return is_a(x, triple[1]) and is_a(source(x), triple[0]) and is_a(target(x), triple[2])
+        except:
+            return False
+        raise TypeError(f"invalid pattern to match on in RP: {triple}")
+
+
+    def has_value_matching(x, vt):
+        my_set = vt.d['absorbed'][0]
+        try:
+            val = value(x)
+        except:
+            raise TypeError(f"HasValue can only be applied to AETs")
+            # TODO: or return false here
+
+        if isinstance(my_set, set):
+            return val in my_set            
+        # If we're here, it should be a VT    
+        return is_a(val, my_set)
+
+
+
     def set_of_matching(el, setof):
         from typing import Callable
-        for set_el in setof.d['absorbed']: 
+        for set_el in setof.d['absorbed'][0]: 
             if set_el == el: return True
         return False
     
@@ -5354,14 +6079,23 @@ class Sentinel: pass
         sentinel = Sentinel() 
         p = typ | absorbed | single | collect
         assert (
-            (isinstance(x, dict) and isinstance(p, dict)) 
+            (isinstance(x, dict) and isinstance(p, dict)) or
+            (type(x) in {list, tuple} and type(p) in {list, tuple}) 
         )
         if isinstance(x, dict):
             for k, v in p.items():            
                 r = x.get(k, sentinel)
                 if r is sentinel: return False
+                if not isinstance(v, ValueType_): raise ValueError(f"The pattern passed didn't have a ValueType but rather {v}")
                 if not is_a(r, v): return False  
             return True
+        elif isinstance(x, list) or isinstance(x, tuple):
+            for p_e, x_e in zip(p, x): # Creates tuples of pairwise elements from both lists
+                if not isinstance(p_e, ValueType_): raise ValueError(f"The pattern passed didn't have a ValueType but rather {v}")
+                if not is_a(x_e, p_e): return False  
+            return True
+        
+        raise NotImplementedError(f"Pattern ValueType isn't implemented for {x}")
 
     def valuetype_matching(el, vt):
         if vt == VT.Any: return True
@@ -5401,6 +6135,12 @@ def valuetype_matching(el, vt):
         if typ.d['type_name'] == "Is":
             return is_matching(x, typ)
 
+        if typ.d['type_name'] == "RP":
+            return rp_matching(x, typ)
+
+        if typ.d['type_name'] == "HasValue":
+            return has_value_matching(x, typ)
+        
         if typ.d['type_name'] == "SetOf":
             return set_of_matching(x, typ)
         
@@ -5408,7 +6148,7 @@ def valuetype_matching(el, vt):
             return not is_a(x, typ.d['absorbed'][0])
 
         if typ.d['type_name'] in  {"Instantiated", "Assigned", "Terminated"}:
-            map_ = {"Instantiated": instantiated, "Assigned": value_assigned, "Terminated": terminated}
+            map_ = {"Instantiated": instantiated, "Assigned": assigned, "Terminated": terminated}
             def compare_absorbed(x, typ):
                 val_absorbed = absorbed(x)
                 typ_absorbed = absorbed(typ)
@@ -5711,15 +6451,6 @@ def filter_type_info(op, curr_type):
 def sort_type_info(op, curr_type):
     return curr_type
 
-def ins_type_info(op, curr_type):
-    return VT.Any # VT.ZefRefs if "ZefRef" in curr_type.d['type_name'] else VT.EZefRefs
-
-def outs_type_info(op, curr_type):
-    return VT.Any # VT.ZefRefs if "ZefRef" in curr_type.d['type_name'] else VT.EZefRefs
-
-def ins_and_outs_type_info(op, curr_type):
-    return VT.Any # VT.ZefRefs if "ZefRef" in curr_type.d['type_name'] else VT.EZefRefs
-
 def source_type_info(op, curr_type):
     return curr_type
 
@@ -5740,8 +6471,6 @@ def instantiation_tx_type_info(op, curr_type):
 def termination_tx_type_info(op, curr_type):
     return VT.ZefRef
 
-def instances_type_info(op, curr_type):
-    return VT.Any # VT.ZefRefs
 
 def uid_type_info(op, curr_type):
     if curr_type == VT.Graph:
@@ -5795,12 +6524,12 @@ def terminate_implementation(z, *args):
 def terminate_type_info(op, curr_type):
     return curr_type
 
-def assign_value_imp(z, val):
+def assign_imp(z, val):
     # We need to keep the assign value as something that works in the GraphDelta
     # code. So we simply wrap everything up as a LazyValue and return that.
-    return LazyValue(z) | assign_value[val]
+    return LazyValue(z) | assign[val]
 
-def assign_value_tp(op, curr_type):
+def assign_tp(op, curr_type):
     return VT.Any
 
 def ET_implementation(z, *args):
@@ -5980,8 +6709,8 @@ def merge_imp(a, second=None, *args):
     https://clojuredocs.org/clojure.core/merge
 
     ---- Examples -----
-    [{'a': 1, 'b': 42}, {'a': 2, 'c': 43}] | merge          # => {'a': 2, 'b': 42, 'c': 43}
-    {'a': 1, 'b': 42} | merge[ {'a': 2, 'c': 43} ]
+    >>> [{'a': 1, 'b': 42}, {'a': 2, 'c': 43}] | merge          # => {'a': 2, 'b': 42, 'c': 43}
+    >>> {'a': 1, 'b': 42} | merge[ {'a': 2, 'c': 43} ]
 
     ---- Signature ----
     List[Dict]          -> Dict
@@ -5989,9 +6718,8 @@ def merge_imp(a, second=None, *args):
     (Dict, Dict, Dict)  -> Dict
 
     ---- Tags ----
-    * tool for: dictionaries
-    * similar: merge_with
-    ...
+    - operates on: Dict
+    - related zefop: merge_with
     """
     from typing import Generator
     if isinstance(a, FlatGraph) and isinstance(second, FlatGraph):
@@ -6030,9 +6758,9 @@ def merge_with_imp(dict_or_list, merge_func, more_dict_or_list=None):
     Based on the Clojure operator https://clojuredocs.org/clojure.core/merge-with
 
     ---- Examples ----
-    {'a': 1, 'b': 2} | merge_with[add][{'a': 3}]                        # => {'a': 4, 'b': 2}
-    {'a': 1, 'b': 2} | merge_with[add][{'a': 3}, {'b': 10, 'c': 5}]     # => {'a': 4, 'b': 12, 'c': 5}    
-    [{'a': [1], 'b': [2]}, {'a': [3]}] | merge_with[concat]             # => {'a': [1, 2], 'b': [3]}
+    >>> {'a': 1, 'b': 2} | merge_with[add][{'a': 3}]                        # => {'a': 4, 'b': 2}
+    >>> {'a': 1, 'b': 2} | merge_with[add][{'a': 3}, {'b': 10, 'c': 5}]     # => {'a': 4, 'b': 12, 'c': 5}    
+    >>> [{'a': [1], 'b': [2]}, {'a': [3]}] | merge_with[concat]             # => {'a': [1, 2], 'b': [3]}
 
     ---- Signature ----
     (Dict[T1, T2], ((T2,T2)->T2), Dict[T1, T2]) -> Dict[T1, T2]
@@ -6065,9 +6793,9 @@ def to_clipboard_imp(x):
     an elementary type to the clipboard.
 
     ---- Examples ----
-    'hello' | to_clipboard                                  # returns an effect
-    my_zef_func | to_clipboard | run                        # copy a single zef function to the clipboard
-    g | now | all[ET.ZEF_Function] | to_clipboard | run     # copy all zef function on graph to clipboard
+    >>> 'hello' | to_clipboard                                  # returns an effect
+    >>> my_zef_func | to_clipboard | run                        # copy a single zef function to the clipboard
+    >>> g | now | all[ET.ZEF_Function] | to_clipboard | run     # copy all zef function on graph to clipboard
 
     ---- Signature ----
     String                          -> Effect
@@ -6231,11 +6959,15 @@ def read_file_imp(fname):
     the effect will contain a "content" key with the file's content.
 
     ---- Examples ----
-    "data.yaml" | read_file | run | get["content"] | from_yaml | collect
+    >>> "data.yaml" | read_file | run | get["content"] | from_yaml | collect
 
     ---- Signature ----
     VT.String -> VT.Effect
 
+    ---- Tags ----
+    - related zefop: load_file
+    - used for: file io
+
     """
     return Effect({
         'type': FX.LocalFile.Read,
@@ -6253,11 +6985,15 @@ def load_file_imp(fname, format = None):
     the effect will contain a "content" key with the transformed object.
 
     ---- Examples ----
-    "data.yaml" | load_file | run | get["content"] | collect
+    >>> "data.yaml" | load_file | run | get["content"] | collect
 
     ---- Signature ----
     VT.String -> VT.Effect
 
+    ---- Tags ----
+    - related zefop: read_file
+    - used for: file io
+
     """
     return Effect({
         'type'     : FX.LocalFile.Load,
@@ -6280,7 +7016,7 @@ def save_file_imp(content, fname, settings = {}):
     An example is for csv, we can pass a dict {"na_values": True} to keep NA values in the Pandas DF.
 
     ---- Examples ----
-    data_as_dict | save_file["data.yaml"] | run
+    >>> data_as_dict | save_file["data.yaml"] | run
     
     ---- Possible issues ---
     data_as_dict | to_yaml | save_file["data.yaml"] | run
@@ -6311,7 +7047,7 @@ def write_file_imp(content, fname):
     which must be passed to run.
     
     ---- Examples ----
-    "Hello" | write_file["data.txt"] | run
+    >>> "Hello" | write_file["data.txt"] | run
     
     ---- Signature ----
     (VT.Any, VT.String) -> VT.Effect
@@ -6370,7 +7106,7 @@ def to_pipeline_imp(ops: list):
 
     ---- Tags ----
     - used for: control flow
-    - operates on: ZefOps, Value Types, Entity, Relation, AtomicEntity, ZefRef, EZefRef
+    - operates on: ZefOp, Value Types, Entity, Relation, AtomicEntity, ZefRef, EZefRef
     - related zefop: inject
     - related zefop: inject_list
     - related zefop: absorbed
@@ -6398,7 +7134,7 @@ def inject_imp(x, injectee):
 
     ---- Tags ----
     - used for: control flow
-    - operates on: ZefOps, Value Types, Entity, Relation, AtomicEntity, ZefRef, EZefRef
+    - operates on: ZefOp, Value Types, Entity, Relation, AtomicEntity, ZefRef, EZefRef
     - related zefop: inject_list
     - related zefop: absorbed
     - related zefop: without_absorbed
@@ -6425,7 +7161,7 @@ def inject_list_imp(v, injectee):
 
     ---- Tags ----
     - used for: control flow
-    - operates on: ZefOps, Value Types, Entity, Relation, AtomicEntity, ZefRef, EZefRef
+    - operates on: ZefOp, Value Types, Entity, Relation, AtomicEntity, ZefRef, EZefRef
     - related zefop: inject
     - related zefop: absorbed
     - related zefop: without_absorbed
@@ -6448,6 +7184,20 @@ def zascii_to_asg_imp(zascii_str: VT.String) -> VT.Dict:
 
     ---- Signature ----
     (VT.String) -> VT.Dict
+
+    ---- Examples ----
+    >>> s = \"""
+    >>>                   RT.RatingScore
+    >>>   ET.Dropdown─────────────────────────►1.0
+    >>>   \"""
+    >>> zascii_to_asg(s)
+
+
+    ---- Tags ----
+    - used for: parsing ascii
+    - operates on: String
+    - related zefop: zascii_to_flatgraph
+    - related zefop: zascii_to_blueprint_fg
     """
     from ...deprecated.tools.zascii import parse_zascii_to_asg
     asg, _  = parse_zascii_to_asg(zascii_str)
@@ -6456,18 +7206,211 @@ def zascii_to_asg_imp(zascii_str: VT.String) -> VT.Dict:
 def zascii_to_asg_tp(op, curr_type):
     return VT.Dict
 
-def zascii_to_schema_imp(zascii_str: VT.String) -> VT.GraphDelta:
+def zascii_to_flatgraph_imp(zascii_str: VT.String) -> VT.FlatGraph:
+    """ 
+    Takes a zascii string representing a Graph and returns a FlatGraph containing all the RAEs 
+    appearing in the string.
+
+    ---- Examples ----
+    >>> s = \"""
+    >>>                   RT.RatingScore
+    >>>   ET.Dropdown─────────────────────────►1.0
+    >>>   \"""
+    >>> zascii_to_flatgraph(s)
+    FlatGraph(
+    (temp_id_0002=>0)
+    (temp_id_0003=>1)
+    (temp_id_0004=>2)
+    -------
+    (0, ET.Dropdown, [2], None)
+    (1, AET.Float, [-2], None, 1.0)
+    (2, RT.RatingScore, [], None, 0, 1)
+    )
+
+    ---- Signature ----
+    (VT.String) -> VT.FlatGraph
+
+    ---- Tags ----
+    - used for: parsing ascii
+    - operates on: String
+    - related zefop: zascii_to_asg
+    - related zefop: zascii_to_blueprint_fg
+    """
+    from ...core.internals import is_any_UID
+
+    asg = zascii_to_asg_imp(zascii_str)
+
+    aet_mapping = {'Float': AET.Float, 'Int': AET.Int, 'Bool': AET.Bool, 'String': AET.String}
+    scalar_types_for_aets = {'Float', 'Int', 'Bool', 'String'}
+    elements = list(asg.items())
+    filter_with_temp_id =filter[lambda p: not is_any_UID(p[1].get('existing_uid', ''))]
+
+    def ensure_valid_aet_type(d: dict):
+        if d[1]['value'] not in scalar_types_for_aets: raise TypeError(f'An AET type "AET.{d.get("value", None)}" is not a parsable AET type')
+        return d
+
+    ets = (elements 
+    | filter[lambda p: p[1]['type'] == 'ET'] 
+    | filter_with_temp_id
+    | map[lambda p: ET(p[1]['value'])[p[0]]]
+    | collect
+    )
+
+    aets = (elements 
+    | filter[lambda p: p[1]['type'] == 'AET'] 
+    | filter_with_temp_id
+    | map[lambda p: ensure_valid_aet_type(p)]
+    | map[lambda p: aet_mapping[p[1]['value']][p[0]]]
+    | collect
+    )
+
+    aets_from_vals = (elements 
+    | filter[lambda p: p[1]['type'] in scalar_types_for_aets]
+    | filter_with_temp_id
+    | map[lambda p: aet_mapping[p[1]['type']][p[0]] <= p[1]['value']]
+    | collect
+    )
+    ids = {absorbed(x)[0] for x in ets + aets}.union({absorbed(x.initial_val)[0] for x in aets_from_vals})
+
+    rels = (elements 
+    | filter[lambda p: p[1]['type'] == 'Edge']
+    | filter_with_temp_id
+    | map[lambda p: (Z[p[1]['source']], RT(asg[p[1]['labeled_by']]['value'])[p[0]], Z[p[1]['target']])]
+    | collect
+    )    
+    sorted_rels = []
+    while rels:
+        for r in rels:
+            if absorbed(r[0])[0] in ids and absorbed(r[2])[0] in ids:
+                sorted_rels.append(r)
+                ids.add(absorbed(r[1])[0])
+                rels.remove(r)
+
+    actions = ets + aets + aets_from_vals + sorted_rels
+    return FlatGraph(actions)
+
+def zascii_to_flatgraph_tp(op, curr_type):
+    return VT.FlatGraph
+
+
+def zascii_to_blueprint_fg_imp(zascii_str: VT.String) -> VT.FlatGraph:
     """ 
-    Takes a zascii string with a schema and returns a GraphDelta.
+    Takes a zascii string representing a Graph Blueprint and returns a FlatGraph containing all the delegates 
+    appearing in the string.
+
+    ---- Examples ----
+    >>> s = \"""
+    >>>                   RT.RatingScore
+    >>>   ET.Dropdown─────────────────────────►AET.Int
+    >>>   \"""
+    >>> zascii_to_blueprint_fg(s)
+    FlatGraph(
+    (D1(ET.Dropdown)=>0)
+    (D1(AET.Int)=>1)
+    (D1({D1(ET.Dropdown)>RT.RatingScore>D1(AET.Int)})=>2)
+    -------
+    (0, D1(ET.Dropdown), [2], None)
+    (1, D1(AET.Int), [-2], None)
+    (2, D1({D1(ET.Dropdown)>RT.RatingScore>D1(AET.Int)}), [], None, 0, 1)
+    )    
 
     ---- Signature ----
-    (VT.String) -> VT.GraphDelta
-    """
-    pass
+    (VT.String) -> VT.FlatGraph
+
+    ---- Tags ----
+    - used for: parsing ascii
+    - operates on: String
+    - related zefop: zascii_to_asg
+    - related zefop: zascii_to_flatgraph
+    """
+    from ...core.internals import is_any_UID
+    asg = zascii_to_asg(zascii_str)
+
+    aet_mapping = {'Float': AET.Float, 'Int': AET.Int, 'Bool': AET.Bool, 'String': AET.String}
+    scalar_types_for_aets = {'Float', 'Int', 'Bool', 'String'}
+    elements = list(asg.items())
+    filter_with_temp_id =filter[lambda p: not is_any_UID(p[1].get('existing_uid', ''))]
+
+    def ensure_valid_aet_type(d: dict):
+        if d[1]['value'] not in scalar_types_for_aets: raise TypeError(f'An AET type "AET.{d.get("value", None)}" is not a parsable AET type')
+        return d
+
+    ets = (elements 
+    | filter[lambda p: p[1]['type'] == 'ET'] 
+    | filter_with_temp_id
+    | map[lambda p: ET(p[1]['value'])[p[0]]]
+    | collect
+    )
+
+    aets = (elements 
+    | filter[lambda p: p[1]['type'] == 'AET'] 
+    | filter_with_temp_id
+    | map[lambda p: ensure_valid_aet_type(p)]
+    | map[lambda p: aet_mapping[p[1]['value']][p[0]]]
+    | collect
+    )
 
-def zascii_to_schema_tp(op, curr_type):
-    return VT.GraphDelta
+    # Don't include the values, could be changes in the future to point to instances
+    aets_without_vals = (elements 
+    | filter[lambda p: p[1]['type'] in scalar_types_for_aets]
+    | filter_with_temp_id
+    | map[lambda p: aet_mapping[p[1]['type']][p[0]] ]
+    | collect
+    )
+
+    id_to_rae = {absorbed(x)[0]: x for x in ets + aets + aets_without_vals}
+
+    rels = (elements 
+    | filter[lambda p: p[1]['type'] == 'Edge']
+    | filter_with_temp_id
+    | map[lambda p: (Z[p[1]['source']], RT(asg[p[1]['labeled_by']]['value'])[p[0]], Z[p[1]['target']])]
+    | collect
+    )    
+
+    # TopoSort the RTs
+    sorted_rels = []
+    while rels:
+        for r in rels:
+            if absorbed(r[0])[0] in id_to_rae and absorbed(r[2])[0] in id_to_rae:
+                sorted_rels.append(r)
+                id_to_rae[absorbed(r[1])[0]] = r[1]
+                rels.remove(r)
+    
+    instance_rep = Delegate
+
+    def get_label(p):
+        return LazyValue(p) | absorbed | single_or[None] | collect
+
+    @func
+    def get_template_representation(p, id_lookup):
+        label = result = None
+        if type(p) == EntityType or type(p) == AtomicEntityType:
+            result = instance_rep(p)
+            label = get_label(p)
+        elif type(p) == tuple:
+            s = get_item(p[0], id_lookup)
+            t = get_item(p[2], id_lookup)
+            result = instance_rep(s, p[1], t)
+            label = get_label(p[1])
+
+        if label is not None:
+            id_lookup[label] = result
+        return result
+
+    def get_item(x, id_lookup):
+        if type(x) == ZefOp and is_a(x, Z):
+            return id_lookup[get_label(x)]
+        if type(x) == Delegate:
+            return x
+        return instance_rep(x)
+    
+    items = concat([ets + aets + aets_without_vals + sorted_rels])
+    reps = items | map[get_template_representation[id_to_rae]] | collect
+    dels = reps | map[delegate_of] | collect
+    return FlatGraph(dels)
 
+def zascii_to_blueprint_fg_tp(op, curr_type):
+    return VT.FlatGraph
 
 #--------------------------------------------------------------------------------------
 def replace_at_imp(str_or_list, index, new_el):
@@ -6475,9 +7418,20 @@ def replace_at_imp(str_or_list, index, new_el):
     Given a list replace element at an index with new_el.
     Given a string return a new string with element at index replaced with new_el.
     
+
+    ---- Examples ----
+    >>> ['a','b','c'] | replace_at[1]['x']    # => ['a','x','c']
+    >>> 'hello' | replace_at[1]['a'] | c      # 'hallo'
+
     ---- Signature ----
     (VT.String, VT.Int, VT.String) -> VT.String
     (VT.List, VT.Int, VT.Any) -> VT.List
+
+    ---- Tags ----
+    - related zefop: replace
+    - used for: list manipulation
+    - operates on: String
+    - operates on: List
     
     """
     from typing import Generator
@@ -6512,19 +7466,6 @@ def replace_at_tp(op, curr_type):
 
 
 
-def pad_to_length_imp(s: VT.String, l: VT.Int) -> VT.String:
-    """ 
-    Pads a string with white space to the right to a specific length l.
-    """
-    print("😞😞😞😞😞  pad_to_length_imp is deprecated. Use 'pad_right' instead")
-    return s + (" " * (l - len(s))) 
-
-def pad_to_length_tp(op, curr_type):
-    return VT.String
-
-
-
-
 
 #---------------------------------------- floor -----------------------------------------------
 def floor_imp(x):
@@ -7050,6 +7991,24 @@ def make_request_tp(op, curr_type):
 
 #---------------------------------------- blake3 -----------------------------------------------
 def blake3_imp(obj) -> VT.String:
+    """
+    Cryptographic hash function.
+    Can be applied to strings and Bytes.
+
+    ---- Examples ----
+    >>> 'hello' | blake3    
+    >>> b'hello' | blake3    
+    
+    ---- Signature ----
+    String -> String
+    Bytes -> String
+
+    ---- Tags ----
+    - used for: Cryptography
+    - used for: Hashing
+    - operates on: String
+    - operates on: Bytes
+    """
     # Hash some input all at once. The input can be bytes, a bytearray, or a memoryview.
     from blake3 import blake3 as b3
     if isinstance(obj, bytes):
@@ -7057,17 +8016,43 @@ def blake3_imp(obj) -> VT.String:
     elif isinstance(obj, str):
         return b3(obj.encode()).hexdigest()
     else:
-        try:
-            from ..op_structs import type_spec
-            type_str = str(type_spec(obj))
-        except:
-            type_str = str(type(obj))
-        return blake3_imp(type_str + str(obj))
+        raise ValueError(f"Expected Bytes or Str but got {obj} instead.")
 
 def blake3_tp(op, curr_type):
     return VT.String
 
 
+def value_hash_imp(obj) -> VT.String:
+    """
+    Cryptographic hash function using blake3 
+    for non-str/bytes values including Zef Values.
+
+    ---- Examples ----
+    >>> 5 | value_hash    
+    >>> g | value_hash    
+    
+    ---- Signature ----
+    Any -> String
+
+    ---- Tags ----
+    - used for: Cryptography
+    - used for: Hashing
+    - operates on: Values
+    - operates on: Any
+    """
+    if isinstance(obj, dict): # Handle dict specially to fix key,value pair order.
+        return blake3_imp("dict" + str(sort(obj.items())))
+    try:
+        from ..op_structs import type_spec
+        type_str = str(type_spec(obj))
+    except:
+        type_str = str(type(obj))
+    return blake3_imp(type_str + str(obj))
+
+def value_hash_tp(op, curr_type):
+    return VT.String
+
+
 #-------------------------------ZEF LIST------------------------------------------
 def to_zef_list_imp(elements: list):
     all_zef = elements | map[lambda v: isinstance(v, ZefRef) or isinstance(v, EZefRef)] | all | collect
@@ -7097,6 +8082,8 @@ def to_zef_list_tp(op, curr_type):
 #-----------------------------FlatGraph Implementations-----------------------------------
 def fg_insert_imp(fg, new_el):
     from ..graph_delta import map_scalar_to_aet_type
+    from ...pyzef.internals import DelegateRelationTriple
+
     assert isinstance(fg, FlatGraph)
     new_fg = FlatGraph()
     new_blobs, new_key_dict = [*fg.blobs], {**fg.key_dict}
@@ -7191,7 +8178,7 @@ def common_logic(new_el):
 
         elif isinstance(new_el, Val):
             new_el = new_el.arg
-            hash_vn = blake3(str(new_el))
+            hash_vn = value_hash(new_el)
             if hash_vn not in new_key_dict:
                 idx = next_idx()
                 new_key_dict[hash_vn] = idx
@@ -7211,6 +8198,30 @@ def common_logic(new_el):
             idx = common_logic(origin_rae(new_el))
             if isinstance(new_blobs[idx][1], AtomicEntityType) and isinstance(new_el, ZefRef):
                 new_blobs[idx] = (*new_blobs[idx][:4], value(new_el))
+        elif isinstance(new_el, Delegate):
+            if isinstance(new_el.item, DelegateRelationTriple):
+                if new_el in new_key_dict:
+                    idx = new_key_dict[new_el]
+                else:
+                    src, rt, trgt = source(new_el), new_el.item.rt, target(new_el)
+                    src_idx = common_logic(src)
+                    trgt_idx = common_logic(trgt)
+                    idx = next_idx()
+
+                    new_blobs.append((idx, new_el, [], None, src_idx, trgt_idx))
+                    if idx not in new_blobs[src_idx][2]: new_blobs[src_idx][2].append(idx)
+                    if idx not in new_blobs[trgt_idx][2]: new_blobs[trgt_idx][2].append(-idx)
+                    new_key_dict[new_el] = idx
+            else:
+                internal_id = new_el | absorbed | attempt[single][None] | collect
+                new_el = new_el | without_absorbed | collect
+                if new_el in new_key_dict:
+                    idx = new_key_dict[new_el]
+                else:
+                    idx = next_idx()
+                    if internal_id: new_key_dict[internal_id] = idx
+                    new_key_dict[new_el] = idx
+                    new_blobs.append((idx, new_el, [], None))
         else:
             idx = None
         return idx
@@ -7227,7 +8238,7 @@ def _insert_dict(new_el):
         return ent_idx
 
     def _insert_single(new_el):
-        if type(new_el) in {EntityType, AtomicEntityType, Entity, AtomicEntity, ZefOp, LazyValue,ZefRef, EZefRef, *list(map_scalar_to_aet_type.keys()), Val}:
+        if type(new_el) in {EntityType, AtomicEntityType, Entity, AtomicEntity, ZefOp, LazyValue,ZefRef, EZefRef, *list(map_scalar_to_aet_type.keys()), Val, Delegate}:
             common_logic(new_el)
         elif isinstance(new_el, tuple) and len(new_el) == 3:
             src, rt, trgt = new_el
@@ -7281,7 +8292,7 @@ def fg_get_imp(fg, key):
     if type(key) in {ZefRef, EZefRef} and origin_uid(key) in kdict: return FlatRef(fg, kdict[origin_uid(key)])
     elif type(key) in {Entity, AtomicEntity} and key.d['uid'] in kdict: return FlatRef(fg, kdict[key.d['uid']])
     elif isinstance(key, Relation) and key.d['uids'][1] in kdict:return FlatRef(fg, kdict[key.d['uids'][1]])
-    elif isinstance(key, Val) and  blake3(key.arg) in kdict: return FlatRef(fg, kdict[blake3(key.arg)])
+    elif isinstance(key, Val) and  value_hash(key.arg) in kdict: return FlatRef(fg, kdict[value_hash(key.arg)])
     elif key in kdict: return FlatRef(fg, kdict[key])
     else: raise KeyError(f"{key} isn't found in this FlatGraph!")
 
@@ -7302,9 +8313,9 @@ def fg_remove_imp(fg, key):
         idx = kdict[key.d['uids'][1]]
         key = key.d['uids'][1]
     elif isinstance(key, Val):
-        if blake3(key.arg) not in kdict: raise error
-        idx = kdict[blake3(key.arg)]
-        key = blake3(key.arg)
+        if value_hash(key.arg) not in kdict: raise error
+        idx = kdict[value_hash(key.arg)]
+        key = value_hash(key.arg)
     elif key in kdict:  
         idx = kdict[key]
     else: raise error
@@ -7337,21 +8348,20 @@ def remove_blob(idx, key = None):
 
 
 def flatgraph_to_commands(fg):
-    return_elements = []
+    return_elements = set()
     idx_key = {idx:key for key,idx in fg.key_dict.items()}
     def dispatch_on_blob(b, for_rt=False):
         idx = b[0]
         if isinstance(b[1], EntityType):
             if idx in idx_key:
                 key = idx_key[idx]
-                if is_a(key, uid):
-                    return Entity({"type": b[1], "uid": key})
+                if is_a(key, uid): return Entity({"type": b[1], "uid": key})
                 else:
                     if for_rt: return Z[key]
                     return b[1][key]
-            elif for_rt or len(b[2]) == 0:
-                return Z[idx]
-            else: return b[1][idx]
+            else:
+                if b[1][idx] in return_elements: return Z[idx]
+                return  b[1][idx] 
         elif isinstance(b[1], AtomicEntityType):
             if idx in idx_key:
                 key = idx_key[idx]
@@ -7362,10 +8372,13 @@ def dispatch_on_blob(b, for_rt=False):
                     if for_rt: return Z[key]
                     if b[-1]: return b[1][key] <= b[-1]
                     else:     return b[1][key]
-            elif for_rt or len(b[2]) == 0:
-                if b[-1]: return b[1][idx] <= b[-1]
-                else:     return b[1][idx]
-            else: return None
+            else:
+                if b[-1]: 
+                    if (b[1][idx] <= b[-1]) in return_elements: return Z[idx] <= b[-1]
+                    return b[1][idx] <= b[-1]
+                else:     
+                    if (b[1][idx]) in return_elements: return Z[idx]
+                    return b[1][idx]
         elif isinstance(b[1], RelationType):
             if idx in idx_key: 
                 key = idx_key[idx]
@@ -7381,24 +8394,25 @@ def dispatch_on_blob(b, for_rt=False):
 
         elif isinstance(b[1], str) and b[1][:2] == "BT":
             if for_rt:
-                return Z[blake3(b[-1])]
+                return Z[value_hash(b[-1])]
             else:
                 from ..graph_delta import map_scalar_to_aet_type
                 if type(b[-1]) in map_scalar_to_aet_type:
                     aet = map_scalar_to_aet_type[type(b[-1])](b[-1])
-                    return aet[blake3(b[-1])] <= (b[-1])
+                    return aet[value_hash(b[-1])] <= (b[-1])
                 else:
-                    return AET.Serialized[blake3(b[-1])] <= to_json(b[-1])
+                    return AET.Serialized[value_hash(b[-1])] <= to_json(b[-1])
+        elif isinstance(b[1], Delegate):
+            return b[1]
 
         raise NotImplementedError(f"Unhandled type in dispatching {b}")
 
     for b in fg.blobs | filter[lambda b: b != None] | collect:
         el = dispatch_on_blob(b)
-        print(b, el)
-        if isinstance(el, LazyValue) or el != None: return_elements.append(el)
+        if isinstance(el, LazyValue) or el != None: return_elements.add(el)
 
     from ..graph_delta import construct_commands
-    return construct_commands(return_elements)
+    return construct_commands(list(return_elements))
 
 
 # ------------------------------FlatRef----------------------------------
@@ -7480,7 +8494,7 @@ def transact_imp(data, g, **kwargs):
         raise ValueError(f"Expected FlatGraph or [] or () for transact, but got {data} instead.")
 
     return Effect({
-            "type": FX.TX.Transact,
+            "type": FX.Graph.Transact,
             "target_graph": g,
             "commands":commands
     })    
@@ -7548,12 +8562,79 @@ def retrieve_or_insert_blob(new_b):
 
 
 
+
+
+
 #-----------------------------Range----------------------------------------
 def range_imp(*args):
-    def generator_wrapper(r):
-        yield from iter(r)
+    """
+    Introduce a separate operator from Python's range.
     
-    return generator_wrapper(range(*args))
+    1)  The lazy output value type should be uniform for other
+    zefops to deal with. `range` does not return a general Python
+    generator, but a special range object. We don't want to  make
+    all other zefops that can operate on a lazy List[Int] to special
+    case range.
+
+    2) we want the ability to pipe the boundary values into `Range`.
+    Range can take a single int or a pair of ints and returns a lazy
+    List[Int].
+
+    3) In contrast to Python's builtin `range`, Zef's `Range` can
+    be called with infinity as an upper bound.
+
+    ---- Signature ----
+    Int -> List[Int]
+    (Int, Int) -> List[Int]
+
+    ---- Examples ----
+    >>> 4 | Range         # => [0,1,2,3]   # but as a generator
+    >>> (2, 5) | Range    # => [2,3,4]
+    >>> Range(4)          # => [0,1,2,3]   # not lazy! The parentheses trigger evaluation for ZefOps!
+    >>> infinity | Range  # => [0,1,2,3, ... # lazy and infinitely long
+    >>> (4, infinity) | Range  # => [4,5,6, ... # lazy and infinitely long
+
+    ---- Gotchas ----
+    - calling with parentheses triggers evaluation. If this is 
+      called for an infinite lazy sequence, the evaluation will not terminate.
+
+    ---- Tags ----
+    - used for: List manipulation
+
+    """
+
+    if len(args) == 1:
+        a = args[0]
+        # 10 | Range | ...
+        if isinstance(a, int) or a==infinity:
+            lo, hi = 0, a        
+        # (2, 5) | Range | ...
+        elif len(a) == 2:
+            if not (isinstance(a[0], int) and (isinstance(a[1], int) or a[1]==infinity)):
+                raise TypeError(f'When called with two argument, Range takes two ints. Got: {a}')
+            else: lo, hi = a
+        else:
+            raise TypeError(f'Range can be called with one or two integers. It got called with the wrong number of args: {a}')
+    # Range(2, 5) | ...
+    elif len(args) == 2:
+        a = args
+        if not (isinstance(a[0], int) and (isinstance(a[1], int) or a[1]==infinity)):
+                raise TypeError(f'When called with two argument, Range takes two ints. Got: {a}')
+        else: lo, hi = a
+    else:
+        raise TypeError(f'Range can be called with one or two integers. It got called with the wrong number of args: {args}')
+
+    def generator_wrapper2():
+        count2 = lo
+        while True:
+            yield count2
+            count2 += 1
+
+    def generator_wrapper1():
+        yield from range(lo, hi)
+    # don't expose the yield directly, keep this a function
+    return generator_wrapper2() if hi==infinity else generator_wrapper1()
+
 
 def range_tp(op, curr_type):
     return None
@@ -7561,6 +8642,13 @@ def range_tp(op, curr_type):
 
 # -----------------------------Old Traversals-----------------------------
 def traverse_implementation(first_arg, *curried_args, func_only, func_multi, func_optional, func_RT, func_BT, traverse_direction):
+    translation_dict = {
+        "outout": "Out",
+        "out"  : "out_rel",
+        "inin": "In",
+        "in": "in_rel",
+    }
+    print(f"Old traversal style will be retired: use `{translation_dict[traverse_direction]}` instead. 🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱🥱 ")
     if isinstance(first_arg, FlatRef):
         return traverse_flatref_imp(first_arg, curried_args, traverse_direction)
     elif isinstance(first_arg, FlatRefs):
@@ -7707,9 +8795,10 @@ def to_bytes_imp(x: String) -> Bytes:
     used for: type conversion
     operates on: String
     """
+    from zef.core.bytes import Bytes_
     if isinstance(x, str): return Bytes(x.encode())     # default: utf8
-    if isinstance(x, Bytes): return x
     if isinstance(x, bytes): return Bytes(x)
+    if isinstance(x, Bytes_): return x
     else: raise NotImplementedError()
 
 
@@ -7867,9 +8956,11 @@ def without_imp(x, y):
 def without_tp(x):
     return VT.Any
 
-def schema_imp(x, include_edges=False):
+def blueprint_imp(x, include_edges=False):
     """ 
-    Returns all schema nodes on the graph or alive in the given GraphSlice.
+    WARNING: the name/interface of this zefop is not stable and is likely to change in the future.
+
+    Returns all delegate nodes on the graph or alive in the given GraphSlice.
     These are the delegate entities/relations which reference all instances on
     the graph.
 
@@ -7878,13 +8969,13 @@ def schema_imp(x, include_edges=False):
     `graphviz`.
  
     ---- Examples ----
-    >>> g | schema[True] | graphviz          # Shows the schema of graph g.
-    >>> g | now | schema[True] | graphviz    # Shows the schema of graph g in the current time slice.
+    >>> g | blueprint[True] | graphviz          # Shows the blueprint of graph g.
+    >>> g | now | blueprint[True] | graphviz    # Shows the blueprint of graph g in the current time slice.
 
-    # A blank graph already has one TX delegate node.
+    >>> # A blank graph already has one TX delegate node.
     >>> g = Graph()
-    >>> g | schema | collect
-    ... [<EZefRef #65 DELEGATE TX at slice=0>]
+    ... g | blueprint | collect
+    [<EZefRef #65 DELEGATE TX at slice=0>]
  
     ---- Signature ----
     (Graph, Bool) => List[EZefRef]
@@ -7894,7 +8985,7 @@ def schema_imp(x, include_edges=False):
     if isinstance(x, Graph):
         g = x
         if include_edges:
-            frontier_of = map_cat[out_rels[BT.TO_DELEGATE_EDGE]] | map_cat[lambda x: (x, target(x))]
+            frontier_of = map_cat[out_rels[BT.TO_DELEGATE_EDGE]] | map_cat[apply[identity, target]]
         else:
             frontier_of = map_cat[Outs[BT.TO_DELEGATE_EDGE]]
 
@@ -7912,12 +9003,509 @@ def step(found, last_frontier):
                      | last
                      | first
                      | collect)
-        return all_items | without[[g | root | collect]] | collect
+
+        all_equal = sliding[2] | map[unpack[equals]] | all
+        is_delegate_rel_group = BT.RELATION_EDGE & Is[apply[identity, source, target] | all_equal]
+        trim_items = [g | root | collect] + (all_items | filter[is_delegate_rel_group] | collect)
+
+        return all_items | without[trim_items | apply[identity, map_cat[out_rels[BT.TO_DELEGATE_EDGE]]] | concat | collect] | collect
     elif isinstance(x, GraphSlice):
-        log.warn("Currently schema(graph_slice) returns eternal schema not the schema in the appropriate reference frame. This will be fixed in the future.")
-        return schema_imp(Graph(x), include_edges) | filter[exists_at[x]] | map[in_frame[x][allow_tombstone]] | collect
+        rae_satisfies = match[
+            (Delegate, exists_at[x]),
+            (Any, aware_of[x])
+        ]
+        satisfies = match[
+            (BT.TO_DELEGATE_EDGE, target),
+            (Any, identity)
+        ] | rae_satisfies
+        return (blueprint_imp(Graph(x), include_edges)
+                # We need to be careful of the filtering here. Only RAEs (including delegates) can be used with exists_at.
+                 | filter[satisfies]
+                # allow_tombstone is set here to bypass exists_at checking
+                 | map[in_frame[x][allow_tombstone]]
+                | collect)
 
-    return Error(f"Don't know how to handle type of {type(x)} in schema zefop")
+    return Error(f"Don't know how to handle type of {type(x)} in blueprint zefop")
+
+
+# ----------------------------- field -----------------------------
+
+@func
+def field_imp(z, rt):
+    """
+    Opinionated operator that is part of high level zef.
+
+    1) If the final node at the end of the path is an AET,
+       the `value` zefop is automatically applied. i.e. The
+       value and not the ZefRef to the node is returned.
+
+    2) Automatically maps to the right nesting order, it
+       essentially wraps with the according number of "map"s
+    >>> [z1, z2, z3] | F.Name      # equivalent to 
+    >>> [z1, z2, z3] | map[Out[RT.Name] | value]
+
+
+    ***** TODO: allow traversing the "implied graphs", given ********
+          a base graph and set of rules.
+
+    rules = [
+            Implies[ (ET.Person['p'], RT.Directed, ET.Movie['m']), (ET.Movie['m'], RT.Director, ET.Person['p']) ],
+
+            Implies[ (Any['y'], RT.BirthYear, Any['x']), (Any['y'], RT.YearOfBirth, Any['x']) ],
+            ]
+    
+    
+    ---- Signature ----
+    (ZefRef, RT) -> ZefRef
+    (List[ZefRef], RT) -> List[ZefRef]
+    (List[List[ZefRef]], RT) -> List[List[ZefRef]]
+
+    ----Examples ----
+    >>> z1 | F.Name         # equivalent to z1 | Out[RT.Name] | value     # if target is an AET
+    >>> z1 | F.FriendOf     # equivalent to z1 | Out[RT.Name]             # if target is an ET or RT
+
+    ---- Tags ----
+    - related zefop: fields
+    - related zefop: Out
+    - used for: graph traversal
+    """
+    def val_maybe(x):
+        if is_a(x, AET): return value(x)
+        else: return x
+
+    if isinstance(z, ZefRef):
+        return val_maybe(Out(z, rt))
+
+    if isinstance(z, list) or isinstance(z, tuple):
+        return [field(zz, rt) for zz in z]
+
+    raise TypeError(f"Field operator not implemented for type(z)={type(z)}    z={z}")
+
+
+# ----------------------------- fields -----------------------------
+
+
+@func
+def fields_imp(z, rt):
+    """
+    Closely related to the `field` operator, but use this for
+    traversing a variable number of outgoing relations, similar
+    to `Outs`.
+
+    If any of the target nodes is an AET, the value is automatically
+    used.
+
+    ---- Signature ----
+    (ZefRef, RT) -> List[ZefRef]
+    (List[ZefRef], RT) -> List[List[ZefRef]]
+
+    ----Examples ----
+    >>> z1 | F.Name         # equivalent to z1 | Out[RT.Name] | value     # if target is an AET
+    >>> z1 | F.FriendOf     # equivalent to z1 | Out[RT.Name]             # if target is an ET or RT
+
+    ---- Tags ----
+    - related zefop: field
+    - related zefop: Outs
+    - used for: graph traversal
+    """
+    def val_maybe(x):
+        if is_a(x, AET): return value(x)
+        else: return x
+
+    if isinstance(z, ZefRef):
+        return [val_maybe(x) for x in Outs(z, rt)]
+
+    if isinstance(z, list) or isinstance(z, tuple):
+        return [fields(zz, rt) for zz in z]
+
+    raise TypeError(f"`fields` operator not implemented for type(z)={type(z)}    z={z}")
+
+
+
+
+# ----------------------------- apply -----------------------------
+def apply_imp(x, f):
+    """
+    Very similar to func to apply a function
+    to an input argument.
+    In addition, a tuple of functions (f1,..,.fn) can 
+    be specified and the output is is the tuple of
+    the individual output values.
+    Note that this procedure is distinct from map, as
+    it acts on the single flow input argument once.
+
+    ---- Signature ----
+    (T,  (T->T2) ) -> T2    # if f is a single function
+    (T, List[(T->T2)] ) -> List[T2]
+
+    ---- Examples ----
+    >>> 40 | apply[
+    ...    add[1],
+    ...    add[2],
+    ...    String,
+    ... ]
+    (41, 42, '40')
+    
+    ---- Tags ----
+    - related zefop: map
+    - related zefop: func
+    - related zefop: call
+    - used for: control flow
+    - used for: function application
+    """
+    if isinstance(f, tuple) or isinstance(f, list):
+        return tuple(ff(x) for ff in f)
+    else: return f(x)
+
+
+
+
+# ----------------------------- split_on_next -----------------------------
+def split_on_next_imp(s, el_to_split_on):
+    """
+    Split a List or a string at one point only
+
+    ---- Examples ----
+    >>> before, after = split_on_next('good morning, good afternoon, good night', ', ')     # => ['good morning', 'good afternoon, good night']
+    >>> [1,2,3,4,5,3,8] | split_on_next[3]    # => (1,2), (4, 5, 3, 8)
+
+    ---- Signature ----
+    (String, String) -> String
+    (List[T], T) -> List[T]
+
+    ---- Tags ----
+    - related zefop: split
+    - related zefop: split_if
+    - related zefop: chunk
+    - operates on: List
+    - operates on: String
+    - used for: list manipulation
+    - used for: string manipulation
+    """
+    from typing import Generator
+    if isinstance(s, str):
+        ind = s.find(el_to_split_on)
+        if ind == -1: return s, ''    # not found
+        return s[:ind], s[ind+len(el_to_split_on):]
+    if isinstance(s, list) or isinstance(s, tuple) or isinstance(s, Generator):
+        it = iter(s)
+        part1 = []
+        while True:
+            val = next(it)
+            if val == el_to_split_on: break
+            part1.append(val)
+        def wrapper():      # keep split_on_next a function
+            yield from it
+        return part1, wrapper()
+
+    raise TypeError(f"expected a String or a List in `split_on_next`, got a {type(s)}")
+
+
+
+# ----------------------------- ops acting on op doctstring -----------------------------
+def examples_imp(op: VT.ZefOp) -> VT.List[VT.Record]:
+    """
+    Returns the examples portion of a docstring as a list of tuples mapping
+    example string to a possible output string.
+
+    It is able to handle multiline result, but not multine examples. This
+    will be later improved.
+
+    ---- Examples ----
+    >>> examples(to_snake_case)
+    [("'yaml-keyword' | to_snake_case", '"yaml_keyword"'),
+    ("'TokenName' | to_snake_case", '"token_name"'),
+    ("'external. data-with  UNUSUAL@characters' | to_snake_case",
+    '"external_data_with_unusualcharacters"')]
+
+    ---- Signature ----
+    (ZefOp) -> List[Record[String | Nil]]
+
+    ---- Tags ----
+    - related zefop: tags
+    - related zefop: signature
+    - related zefop: related_ops
+    - related zefop: operates_on
+    - related zefop: used_for
+    - operates on: ZefOp
+    - used for: op usage
+    """
+    def parse_example(s):
+        s = trim(s, " ")
+
+        # Case of empty lines
+        if len(s) == 0: return None
+
+        # Case of comments
+        if s[0] == "#": raise ValueError(f"Comments aren't allowed on new-lines without >>> before them so not to be confused with an output that starts with #! {s}")
+
+        # ... <example one-liner 2>
+        if s[:3] == "...":
+            example = s[3:] | trim[" "] | collect
+            comment_idx = example.find('#')
+            if comment_idx != -1: example = example[:comment_idx] | trim[" "] | collect
+            return (example, None, "multi-example")
+
+        # case of example lines
+        if s[:3] == ">>>":
+            result = None
+            s = s | trim[">>>"] | trim[" "] | collect
+
+            # case of '>>>' only
+            if len(s) == 0: return None                 
+
+            # case of '>>> <example one-liner 1>       # => <result of the one-liner (a value)>'
+            s = s.replace("# ->", "# =>").replace("=>", "# =>") # TODO relax this later i.e make it break on malformation
+            result_idx = s.find('# =>')
+            if result_idx != -1:
+                result = s[result_idx + 4:] | trim[" "] | collect
+                comment_idx = result.find('#')
+                if comment_idx != -1: result = result[:comment_idx] | trim[" "] | collect
+                s = s[:result_idx] | trim[" "] | collect
+
+            # case of '>>> <example one-liner 2>       # <comment on the behaviour (not a value)>'
+            comment_idx = s.find('#')
+            if comment_idx != -1:
+                s = s[:comment_idx] | trim[" "] | collect
+                if len(s) == 0: return None
+
+            return s, result
+
+        # case of '(single line or multine output )'
+        else:
+            return (None, s, "multi-result")
+
+    def process_examples(tups):
+        def join_if_not_empty(s1, s2):
+            if s2: return s1 + "\n" + s2
+            return s1
+
+        result = []
+        curr_result = curr_example = ""
+        for t in reversed(list(tups)):
+            if t[-1] and t[-1] == "multi-result": 
+                curr_result = join_if_not_empty(t[1], curr_result)
+                # curr_result = t[1] + "\n" + curr_result
+            elif t[-1] and t[-1] == "multi-example": 
+                curr_example = join_if_not_empty(t[0], curr_example)
+                # curr_example = t[0] + "\n" + curr_example
+            elif not t[-1]:
+                example = join_if_not_empty(t[0], curr_example)
+                result.append((example, curr_result))
+                curr_result = curr_example = ""
+            else:
+                if curr_example or curr_result: raise ValueError(f"Malformed example. A multiline output or example contained also an inline comment. This isn't allowed! {t}")
+                result.append(t)
+        return result[::-1]
+
+
+    s = docstring(op) | split["\n"] | collect
+    example_idx = s.index("---- Examples ----")
+    return (
+        s 
+        | skip[example_idx + 1] 
+        | take_while[lambda l: l[:4] != "----"] 
+        | map[parse_example]
+        | filter[lambda l: l != None]
+        | func[process_examples]
+        | collect
+    )
+
+
+def signature_imp(op: VT.ZefOp) -> VT.List[VT.String]:
+    """
+    Given the signature portion of a docstring is valid, returns back a List of Strings.
+
+    ---- Examples ----
+    >>> signature(apply)
+    ['(T,  (T->T2) ) -> T2', '(T, List[(T->T2)] ) -> List[T2]']
+
+    >>> signature(schema)
+    ['(Graph, Bool) -> List[EZefRef]', '(GraphSlice, Bool) -> List[ZefRef]']
+
+    ---- Signature ----
+    (ZefOp) -> List[String]
+
+    ---- Tags ----
+    - related zefop: tags
+    - related zefop: examples
+    - related zefop: related_ops
+    - related zefop: operates_on
+    - related zefop: used_for
+    - operates on: ZefOp
+    - used for: op usage
+    """
+    def clean_and_eval(s):
+        s = s | replace['=>']['->'] | collect
+        commment_idx = s.find("#")
+        if commment_idx != -1: s = s[:commment_idx]
+        return s | trim[' '] | join | collect
+        # extra_eval = lambda expr: eval(expr, globals(), {**VT.__dict__})
+        # try:
+            # val_type = extra_eval(s)
+        # except:
+            # raise ValueError(f"The following '{s}'' signature is malformed and can't be parsed! Make sure all types present in the signature are valid ValueTypes")
+        # return val_type
+
+
+    s = LazyValue(op) | docstring | split["\n"] | collect
+    try:
+        signature_idx = s.index("---- Signature ----")
+    except:
+        raise ValueError(f"The docstring for {op} is either malformed or missing a Signature section!")  from None
+    signature = (
+        s 
+        | skip[signature_idx + 1] 
+        | take_while[lambda l: l[:4] != "----"] 
+        | filter[lambda l: l != ""]
+        | map[clean_and_eval]
+        | collect
+    )
+    return signature
+
+
+def tags_imp(op: VT.ZefOp) -> VT.List:
+    """
+    Returns the tags portion of a docstring as a list of list of strings.
+
+    ---- Examples ----
+    >>> tags(map)
+    [['used for', 'control flow'],
+    ['used for', 'function application'],
+    ['related zefop', 'apply_functions']]
+
+    ---- Signature ----
+    (ZefOp) -> List[String]
+
+    ---- Tags ----
+    - related zefop: signature
+    - related zefop: examples
+    - related zefop: related_ops
+    - related zefop: operates_on
+    - related zefop: used_for
+    - operates on: ZefOp
+    - used for: op usage
+    """
+    try:
+        return (LazyValue(op) 
+        | docstring 
+        | split["\n"]
+        | skip_while[Not[equals['---- Tags ----']]] 
+        | skip[1]
+        | take_while[lambda s: s[:4]!='----']
+        | map[trim_left['-']]
+        | filter[~SetOf('')]
+        | map[split[':'] | map[trim[' ']]]
+        | collect
+    )
+    except:
+        return []
+
+
+def related_ops_imp(op: VT.ZefOp) -> VT.List[VT.ZefOp]:
+    """
+    Extracts the related ops from the tags portion of the docstring of the op.
+    It returns back a list of ZefOp. It also discards invalid ZefOps.
+
+    ---- Examples ----
+    >>> related_ops(to_snake_case)
+    [to_lower_case, to_upper_case, to_pascal_case, to_camel_case, to_kebab_case, to_screaming_snake_case]
+
+    ---- Signature ----
+    (ZefOp) -> List[ZefOp]
+
+    ---- Tags ----
+    - related zefop: signature
+    - related zefop: examples
+    - related zefop: tags
+    - related zefop: operates_on
+    - related zefop: used_for
+    - operates on: ZefOp
+    - used for: op usage
+    """
+    from ... import ops
+    tags_lines = tags(op) | collect
+    return (
+        tags_lines 
+        | filter[first |equals["related zefop"]]
+        | map[second | func[lambda s: ops.__dict__.get(s, None)]]
+        | filter[lambda el: el != None]
+        | collect
+    )
+
+def operates_on_imp(op: VT.ZefOp) -> VT.List[VT.ValueType]:
+    """
+    Extracts the operates on Types from the tags portion of the docstring of the op.
+    It returns back a list of ValueTypes. It also discards invalid ValueTypes.
+
+    ---- Examples ----
+    >>> operates_on(blake3)
+    [String, Bytes]
+
+    ---- Signature ----
+    (ZefOp) -> List[ValueType]
+
+    ---- Tags ----
+    - related zefop: signature
+    - related zefop: examples
+    - related zefop: tags
+    - related zefop: related_ops
+    - related zefop: used_for
+    - operates on: ZefOp
+    - used for: op usage
+    """
+    tags_lines = tags(op) | collect
+    return (
+        tags_lines 
+        | filter[first |equals["operates on"]]
+        | map[second | func[lambda s: VT.__dict__.get(s, None)]]
+        | filter[lambda el: el != None]
+        | collect
+    )
+
+def used_for_imp(op: VT.ZefOp) -> VT.List[VT.String]:
+    """
+    Extracts the used for from the tags portion of the docstring of the op.
+
+    ---- Examples ----
+    >>> used_for(append)   # => ["list manipulation", "string manipulation"]
+
+    ---- Signature ----
+    (ZefOp) -> List[String]
+
+    ---- Tags ----
+    - related zefop: signature
+    - related zefop: examples
+    - related zefop: tags
+    - related zefop: related_ops
+    - related zefop: used_for
+    - operates on: ZefOp
+    - used for: op usage
+    """
+    tags_lines = tags(op) | collect
+    return (
+        tags_lines 
+        | filter[first |equals["used for"]]
+        | map[second]
+        | collect
+    )
+
+
+
+
+def indexes_of_imp(v, ElType):
+    """
+    Given a list, returns the indexes of the elements which are of
+    a specified logic type / set.
+
+    ---- Signature ----
+    (List[Any], ValueType) -> List[Int]
+
+    ---- Examples ----
+    >>> me = ["foo", 5,"bar", "baz", 'bar'] | indexes_of[SetOf["bar",]] | c  # => [2, 4]
+
+    ---- Tags ----
+    operates on: List
+    """
+    return v | enumerate | filter[second | is_a[ElType]] | map[first] | collect
 
-def schema_tp(x):
-    return VT.List
diff --git a/python/zef/core/op_implementations/yo.py b/python/zef/core/op_implementations/yo.py
index 21567e61..70e97df8 100644
--- a/python/zef/core/op_implementations/yo.py
+++ b/python/zef/core/op_implementations/yo.py
@@ -19,7 +19,7 @@
 from ..internals import is_delegate, root_node_blob_index, BlobType
 from .._core import *
 from .. import internals
-from ..VT import TX,String, Instantiated, Terminated, Assigned
+from ..VT import TX,String, Instantiated, Terminated, Assigned, Is
 
 def yo_implementation(x, display=True):
     import inspect
@@ -215,13 +215,8 @@ def readable_datetime_from_tx_uzr(uzr_tx) -> str:
     uzr_tx = to_ezefref(uzr_tx)
     if index(uzr_tx) == root_node_blob_index():
         return '/'
-    return str(time_slice(to_graph_slice(uzr_tx))) + ': ' + str(uzr_tx | time | collect)
+    return f'GraphSlice {str(graph_slice_index(to_graph_slice(uzr_tx)))}: {str(uzr_tx | time | collect)}'
 
-    # time_stamp_here = unix_time(uzr_tx)
-    # return datetime.fromtimestamp(time_stamp_here)\
-    #     .astimezone(timezone(timedelta(hours=8)))\
-    #     .strftime(f"TimeSlice {fill_str_to_length(str(time_slice(uzr_tx).value), 6)} %d.%m.%Y %H:%M:%S (SG), {time_stamp_here}") \
-    #     if index(uzr_tx) != internals.root_node_blob_index() else "/"
 
 
 def eternalist_view(zr_or_uzr) -> str:
@@ -337,7 +332,7 @@ def edge_time_by_type(edge_info_triple: tuple) -> Time:
             return (edge | instantiation_tx | time | collect) if edge_state == "Instantiated" else (edge | termination_tx | time | collect)
         return edge | source | time | collect
 
-    reference_tx_timeslice = time_slice(to_graph_slice(reference_tx))
+    reference_tx_gs_index = graph_slice_index(to_graph_slice(reference_tx))
     rel_ent_inst_edge = uzr | in_rel[BT.RAE_INSTANCE_EDGE]
     all_edges = [(e, "low_lvl", "") for e in rel_ent_inst_edge | in_rels[BT] | collect]
     add_directed_rt_to_list(uzr | in_rels[RT] | filter[lambda z: BT(z) != BT.RAE_INSTANCE_EDGE] | collect, "in")
@@ -348,7 +343,7 @@ def edge_time_by_type(edge_info_triple: tuple) -> Time:
     # Loop over all edges and filter based on edge_type and on the reference_tx value
     for e in all_edges:
         edge, edge_type, edge_state = e
-        if edge_type == "low_lvl" and edge | source | to_graph_slice | time_slice | collect <= reference_tx_timeslice:
+        if edge_type == "low_lvl" and edge | source | to_graph_slice | graph_slice_index | collect <= reference_tx_gs_index:
             edge_description = (f"{fill_str_to_length(generate_low_level_description(edge), 65)}  "
                                 f"[{readable_datetime_from_tx_uzr(edge | source | collect)}]", edge)
             if BT(edge) == BT.TERMINATION_EDGE:
@@ -356,8 +351,8 @@ def edge_time_by_type(edge_info_triple: tuple) -> Time:
             else:
                 descriptions_and_txs.append(edge_description)
         else:
-            if (edge_state == "Instantiated" and time_slice(edge | instantiation_tx | frame | collect) <= reference_tx_timeslice) or\
-                    (edge_state == "Terminated" and time_slice(edge | termination_tx | frame | collect) <= reference_tx_timeslice):
+            if (edge_state == "Instantiated" and graph_slice_index(edge | instantiation_tx | frame | collect) <= reference_tx_gs_index) or\
+                    (edge_state == "Terminated" and graph_slice_index(edge | termination_tx | frame | collect) <= reference_tx_gs_index):
                 descriptions_and_txs.append(
                     (f"{fill_str_to_length(generate_rt_description(edge, edge_type, edge_state), 54)}", edge))
 
@@ -379,13 +374,19 @@ def edge_time_by_type(edge_info_triple: tuple) -> Time:
 
 def graph_info(g) -> str:
     bl = blobs(g)
+    grouped_by_bts =  dict(
+        bl 
+        | group_by[BT] 
+        | filter[lambda x: x[0] in {BT.TX_EVENT_NODE, BT.ENTITY_NODE, BT.ATOMIC_ENTITY_NODE, BT.RELATION_EDGE}] 
+        | collect
+    )
 
     def simple_lengths(g) -> str:
         return (f"{g.graph_data.write_head} blobs, "
-                + str(length(bl | filter[is_a[BT.TX_EVENT_NODE]])) + " TXs, "
-                + str(length(bl | filter[is_a[BT.ENTITY_NODE]])) + " ETs, "
-                + str(length(bl | filter[is_a[BT.ATOMIC_ENTITY_NODE]])) + " AETs, "
-                + str(length(bl | filter[is_a[BT.RELATION_EDGE]])) + " RTs"
+                + str(length(grouped_by_bts[BT.TX_EVENT_NODE])) + " TXs, "
+                + str(length(grouped_by_bts[BT.ENTITY_NODE])) + " ETs, "
+                + str(length(grouped_by_bts[BT.ATOMIC_ENTITY_NODE])) + " AETs, "
+                + str(length(grouped_by_bts[BT.RELATION_EDGE])) + " RTs"
                 )
 
     from builtins import round
@@ -395,18 +396,18 @@ def simple_lengths(g) -> str:
 ===================================== Seen from: {str(now())} =========================================
 ======================================================================================================================
 
-instantiation:          {readable_datetime_from_tx_uzr(g | All[TX] | first | collect) if length(g | All[TX] | collect) > 0 else "NA"}
-last change:            {readable_datetime_from_tx_uzr(g | All[TX] | last | collect)  if len(g | All[TX] | collect) > 0 else "NA"}
+instantiation:          {readable_datetime_from_tx_uzr(g | all[TX] | first | collect) if length(g | all[TX] | collect) > 0 else "NA"}
+last change:            {readable_datetime_from_tx_uzr(g | all[TX] | last | collect)  if len(g | all[TX] | collect) > 0 else "NA"}
 current tags:           {g.graph_data.tag_list}
 summary:                {simple_lengths(g)}
 size:                   {round((g.graph_data.write_head * 16) / 1E6, 3)}MB
 
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Atomic Entities ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-{type_summary_view(bl, g, BT.ATOMIC_ENTITY_NODE)}
+{type_summary_view(grouped_by_bts[BT.ATOMIC_ENTITY_NODE], g, BT.ATOMIC_ENTITY_NODE)}
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Entities ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-{type_summary_view(bl, g, BT.ENTITY_NODE)}
+{type_summary_view(grouped_by_bts[BT.ENTITY_NODE], g, BT.ENTITY_NODE)}
 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  Relations ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
-{type_summary_view(bl, g, BT.RELATION_EDGE)}
+{type_summary_view(grouped_by_bts[BT.RELATION_EDGE], g, BT.RELATION_EDGE)}
 """
 
 
@@ -422,7 +423,6 @@ def triple_string_view(summary: Tuple[int, int, tuple]) -> str:
 
     def find_alive_count_of_triple(triple: Tuple[str]) -> int:
         d_top = delegate_of(triple[1], g)
-
         return (d_top
          | Outs[BT.TO_DELEGATE_EDGE]
          | filter[And[source | rae_type | equals[triple[0]]]
@@ -431,29 +431,41 @@ def find_alive_count_of_triple(triple: Tuple[str]) -> int:
          | sum
          | collect)
 
-    def find_triples_of_rt(rt: str) -> str:
-        return "".join(
-            seq([(rae_type(z | source | collect), rae_type(z), rae_type(z | target | collect)) for z in
-                bl | filter[is_a[RT(rt)]] | filter[lambda z: not internals.is_delegate(z)]])
-            .group_by(lambda x: x)
-            .map(lambda x: (x[0], len(x[1]), find_alive_count_of_triple(x[0])))
-            .map(triple_string_view)
+    def rt_block_view(rt: str, total_count: int) -> str:
+        rt = RT(rt)
+        total_alive = 0
+        def find_triple_count_and_inc(trip):
+            nonlocal total_alive
+            alive = find_alive_count_of_triple(trip)
+            total_alive += alive
+            return alive
+
+        triples_str_views = (
+            grouped_triples
+            | filter[lambda x: x[0][1] == rt]
+            | map[lambda x: (x[0], len(x[1]), find_triple_count_and_inc(x[0]))]
+            | map[triple_string_view]
+            | join[""]
+            | collect
         )
-    # TODO find correct alive instances for RT
-    return "".join(
-        seq([z for z in bl | filter[is_a[bt_filter]] | filter[lambda z: not internals.is_delegate(z)]])
-        .group_by(lambda z: zr_type(z))
-        .map(lambda x: (x[0], len(x[1]), len(x[1][0] | delegate_of | now | all | collect)))
-        .map(lambda x: aet_et_rt_string_view(x) + find_triples_of_rt(
-            x[0][3:]) if bt_filter == BT.RELATION_EDGE else aet_et_rt_string_view(x))
+        return aet_et_rt_string_view((repr(rt), total_count, total_alive)) + triples_str_views
+
+    filtered_blobs = bl | filter[lambda b: not is_a(b, Delegate)]
+    if bt_filter == BT.RELATION_EDGE:
+        grouped_triples = filtered_blobs | map[lambda z: (rae_type(z | source | collect), rae_type(z), rae_type(z | target | collect))] | group_by[lambda x: x] | collect
+    else: grouped_triples = None
+
+    return (
+        [z for z in filtered_blobs]
+        | group_by[zr_type]
+        | map[lambda x: (x[0], len(x[1]), len(x[1][0] | delegate_of | now | all | collect))]
+        | map[lambda x: rt_block_view(x[0][3:], x[1]) if bt_filter == BT.RELATION_EDGE else aet_et_rt_string_view(x)]
+        | join[""]
+        | collect
     )
-
-
 # +\
 # indent_lines(relations_view(zr_or_uzr)) + "\n" +\
 # indent_lines(timeline_view(zr_or_uzr))
-
-
 # def src_dst_info(uzr) -> str:
 #     return f"""
 #     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Source and Target ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
diff --git a/python/zef/core/op_structs.py b/python/zef/core/op_structs.py
index 18a2a613..60b79bf2 100644
--- a/python/zef/core/op_structs.py
+++ b/python/zef/core/op_structs.py
@@ -78,7 +78,7 @@
 ********************************* Expect Operator *********************************
 Introduce the 'expect' operator?
 In C++ one may consider making it templated.
-my_zefop | expect[Int] | ...
+my_zefop | ensureInt] | ...
 Will throw if during evaluation a value of a different type passes through.
 Can be used as an important tool by the runtime operator introspector, e.g.
 if no types can be inferred due to missing type annotations in functions.
@@ -135,7 +135,7 @@
 
 
 my_op1 = (
- expect[Awaitable[Dict]]
+ ensureAwaitable[Dict]]
  | map[my_f]
  | merge
  | group_by[get_value['type']]
@@ -228,7 +228,6 @@ def is_python_scalar_type(o):
     return type(o) in {str, bytes, int, bool, float, type(None)}
 
 def is_supported_value(o):
-    from .fx.fx_types import _Effect_Class
     from .VT import ValueType_
     from . import  GraphSlice
     from types import GeneratorType
@@ -236,8 +235,9 @@ def is_supported_value(o):
     from .error import _ErrorType
     from ..pyzef.main import Keyword
     from ..core.bytes import Bytes_
+    from types import ModuleType
     if is_python_scalar_type(o): return True
-    if type(o) in {set, range, GeneratorType, list, tuple, dict, _Effect_Class, ValueType_, GraphSlice, Time, Image, Bytes_, _ErrorType, Keyword}: return True
+    if type(o) in {set, range, GeneratorType, list, tuple, dict, ValueType_, GraphSlice, Time, Image, Bytes_, _ErrorType, Keyword, ModuleType}: return True
     return False
 
 def is_supported_zef_value(o):
@@ -290,8 +290,8 @@ def __repr__(self):
     def __le__(self, other):
         # we want to allow for "AET.Float['some_name'] <= 42.1"
         # We convert the first expression to a "instantiated[AET.Float]['some_name']"
-        from ._ops import assign_value
-        return LazyValue(self) | assign_value[other]
+        from ._ops import assign
+        return LazyValue(self) | assign[other]
     
     def __ror__(self, other):
         if isinstance(other, TraversableABC):
@@ -310,7 +310,7 @@ def __ror__(self, other):
             return LazyValue(other) | self
         # This is just for a bit of help for users to understand what's going on
         elif isinstance(other, Iterable):
-            raise TypeError("An arbitrary iterable will not be automatically converted to a LazyValue when being piped into a ZefOp. This is because it cannot be determined if it is a stateful iterator (e.g. a generator or a file object) or a stateless iterable (e.g. a list or a range() object).\n\nIf you know your iterable is immutable, then wrap the iterable in `LazyValue`, that is use `LazyValue(itr) | zefop` instead of `itr | zefop`.\n\nIf your iterable is an iterator that mutates on iteration you should either\n\ta) first wrap it in a caching mechanism (TODO make this available in zef: not available in any external library that I can see) or\n\tb) make its content available as a Stream, e.g. TODO.")
+            raise TypeError(f"An arbitrary iterable (we saw {type(other)}) will not be automatically converted to a LazyValue when being piped into a ZefOp. This is because it cannot be determined if it is a stateful iterator (e.g. a generator or a file object) or a stateless iterable (e.g. a list or a range() object).\n\nIf you know your iterable is immutable, then wrap the iterable in `LazyValue`, that is use `LazyValue(itr) | zefop` instead of `itr | zefop`.\n\nIf your iterable is an iterator that mutates on iteration you should either\n\ta) first wrap it in a caching mechanism (TODO make this available in zef: not available in any external library that I can see) or\n\tb) make its content available as a Stream, e.g. TODO.")
 
         return NotImplemented
         
@@ -879,8 +879,8 @@ def __rshift__(self, other):
         return self.lt_gt_lshift_rshift_behavior(other, RT.OutOutOld) 
 
     def __le__(self, value):
-        from ._ops import assign_value
-        return self | assign_value[value]
+        from ._ops import assign
+        return self | assign[value]
 
     def __iter__(self):
         return iter(self.evaluate(unpack_generator = False))
@@ -932,9 +932,8 @@ def evaluate(self, unpack_generator = True):
 
         for op in self.el_ops.el_ops: 
             if op[0] == RT.Collect: continue
-            if op[0] == RT.Run:  
-                from .fx.fx_types import _Effect_Class
-                if isinstance(curr_value, _Effect_Class): 
+            if op[0] == RT.Run:
+                if isinstance(curr_value, dict): 
                     curr_value = _op_to_functions[op[0]][0](curr_value)
                 elif len(op[1]) > 1: # i.e run[impure_func]
                     curr_value = op[1][1](curr_value)
@@ -1091,7 +1090,6 @@ def type_spec_tuple(obj):
 
 def type_spec(obj, no_type_casting = False):
     from .VT import ValueType_
-    from .fx.fx_types import _Effect_Class
     from . import GraphSlice
     from rx.subject import Subject
     from rx.core import Observable
@@ -1114,7 +1112,6 @@ def type_spec(obj, no_type_casting = False):
         Time:                       VT.Time,  
         Subject:                    VT.Awaitable,
         Observable:                 VT.Awaitable,
-        _Effect_Class:              VT.Effect,
         GraphSlice:                 VT.GraphSlice,
     }.get(t, lambda o: ValueType_(type(o).__name__, 0))
     try:
diff --git a/python/zef/core/patching.py b/python/zef/core/patching.py
index fdc24bab..8a1623dc 100644
--- a/python/zef/core/patching.py
+++ b/python/zef/core/patching.py
@@ -24,37 +24,36 @@
 main.Time.__hash__ = lambda self: hash(self.seconds_since_1970)
 
 internals.Delegate.__hash__ = lambda self: hash((self.order, self.item))
-internals.DelegateEntity.__hash__ = lambda self: hash(self.et)
-internals.DelegateAtomicEntity.__hash__ = lambda self: hash(self.aet)
-internals.DelegateRelationGroup.__hash__ = lambda self: hash(self.rt)
 internals.DelegateRelationTriple.__hash__ = lambda self: hash((self.rt, self.source, self.target))
 internals.DelegateTX.__hash__ = lambda self: hash(internals.DelegateTX)
 internals.DelegateRoot.__hash__ = lambda self: hash(internals.DelegateRoot)
 
 
-def or_for_EntityType(self, other):
+from ..pyzef.main import EntityType, RelationType
+from ..pyzef.internals import AtomicEntityType, EntityTypeStruct, RelationTypeStruct, AtomicEntityTypeStruct, BlobTypeStruct, BlobType
+override_types = (EntityTypeStruct, RelationTypeStruct, AtomicEntityTypeStruct, BlobTypeStruct, BlobType, AtomicEntityType, EntityType, RelationType)
+
+def or_for_types(self, other):
     from . import ValueType_
-    from zef.core import EntityType, RelationType, AtomicEntityType
-    from zef.core.internals import EntityTypeStruct, RelationTypeStruct, AtomicEntityTypeStruct, BlobTypeStruct, BlobType
-    allowed_types = (ValueType_, EntityTypeStruct, RelationTypeStruct, AtomicEntityTypeStruct, BlobTypeStruct, BlobType, AtomicEntityType, EntityType, RelationType)
+    allowed_types = (ValueType_,) + override_types
     if isinstance(other, allowed_types):
         return ValueType_(type_name='Union', absorbed=(self, other,))
 
     return NotImplemented
     
-def and_for_EntityType(self, other):
+def and_for_types(self, other):
     from . import ValueType_
-    from zef.core import EntityType, RelationType, AtomicEntityType
-    from zef.core.internals import EntityTypeStruct, RelationTypeStruct, AtomicEntityTypeStruct, BlobTypeStruct, BlobType
-    allowed_types = (ValueType_, EntityTypeStruct, RelationTypeStruct, AtomicEntityTypeStruct, BlobTypeStruct, BlobType, AtomicEntityType, EntityType, RelationType)
+    allowed_types = (ValueType_,) + override_types
     if isinstance(other, allowed_types):
         return ValueType_(type_name='Intersection', absorbed=(self, other,))
 
     return NotImplemented
 
-EntityType.__or__ = or_for_EntityType
-EntityType.__ror__ = or_for_EntityType
-EntityType.__and__ = and_for_EntityType
+for x in override_types:
+    x.__or__ = or_for_types
+    x.__ror__ = lambda x,y: or_for_types(y,x)
+    x.__and__ = and_for_types
+    x.__rand__ = lambda x,y: and_for_types(y,x)
 
 
 # FIXME: why do we need the standard list of timezones in the docstring for Time? Should just refer people to the TZ database. I have removed it for now.
@@ -247,8 +246,8 @@ def wrap_repr(typ):
 # catch the case "AET.Float['abc'] <= 42.1"
 # For consistency, we use Lazy ZefOps as intermediate containers
 def leq_monkey_patching_ae(self, other):
-    from ._ops import assign_value, instantiated, LazyValue
-    return LazyValue(self) | assign_value[other]
+    from ._ops import assign, LazyValue
+    return LazyValue(self) | assign[other]
     
 AtomicEntityType.__le__ = leq_monkey_patching_ae
 
@@ -274,11 +273,11 @@ def leq_monkey_patching_ae(self, other):
 # main.ZefRefs._repr_pretty_ = pprint_ZefRefs
 # main.EZefRefs._repr_pretty_ = pprint_ZefRefs
 
-def convert_to_assign_value(self, value):
-    from ._ops import assign_value
-    return self | assign_value[value]
-ZefRef.__le__ = convert_to_assign_value
-EZefRef.__le__ = convert_to_assign_value
+def convert_to_assign(self, value):
+    from ._ops import assign
+    return self | assign[value]
+ZefRef.__le__ = convert_to_assign
+EZefRef.__le__ = convert_to_assign
 
 original_Graph__contains__ = main.Graph.__contains__
 def Graph__contains__(self, x):
diff --git a/python/zef/core/pure_utils.py b/python/zef/core/pure_utils.py
new file mode 100644
index 00000000..80812f23
--- /dev/null
+++ b/python/zef/core/pure_utils.py
@@ -0,0 +1,20 @@
+# Copyright 2022 Synchronous Technologies Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+
+def get_in_pure(d, path, default_val):
+    if len(path) == 0: return d
+    if type(d) != dict: return default_val
+    return get_in_pure(d.get(path[0], default_val), path[1:], default_val)
\ No newline at end of file
diff --git a/python/zef/core/serialization.py b/python/zef/core/serialization.py
index 647e3052..b06ec8e5 100644
--- a/python/zef/core/serialization.py
+++ b/python/zef/core/serialization.py
@@ -24,9 +24,10 @@
 from .abstract_raes import Entity, Relation, AtomicEntity
 from .error import _ErrorType, Error
 from .image import Image
-from .fx.fx_types import _Effect_Class, FXElement, Effect
+from .fx.fx_types import FXElement, Effect
 from .flat_graph import FlatGraph, FlatRef, FlatRefs
 from ..pyzef import internals as pyinternals
+from .VT import ValueType_
 
 ##############################
 # * Definition
@@ -106,6 +107,7 @@ def deserialize_internal(v):
     This function is the recursive core of the deserialization, where the
     objects are not wrapped in the serialization header.
     """
+    from .logger import log
     if isinstance(v, dict) and "_zeftype" in v:
         v = deserialization_mapping[v["_zeftype"]](v)
     elif isinstance(v, dict):
@@ -237,9 +239,6 @@ def serialize_zeftypes(z) -> dict:
     elif isinstance(z, _ErrorType):
         return {"_zeftype": "ErrorType", "type": z.name, "args": serialize_list(z.args)}
 
-    elif isinstance(z, _Effect_Class):
-        return {"_zeftype": "Effect", "internal_dict": serialize_dict(z.d)}
-    
     elif isinstance(z, FXElement):
         return {"_zeftype": "FXElement", "elements": [e for e in z.d]}
 
@@ -248,19 +247,13 @@ def serialize_zeftypes(z) -> dict:
 
 def serialize_delegate(z) -> dict:
     if isinstance(z, Delegate):
-        return {"_zeftype": "Delegate", "order": z.order, "item": serialize_delegate(z.item)}
-    elif isinstance(z, pyinternals.DelegateEntity):
-        return {"_zeftype": "DelegateEntity", "et": z.et}
-    elif isinstance(z, pyinternals.DelegateAtomicEntity):
-        return {"_zeftype": "DelegateAtomicEntity", "aet": z.aet}
-    elif isinstance(z, pyinternals.DelegateRelationGroup):
-        return {"_zeftype": "DelegateRelationGroup", "rt": z.rt}
+        return {"_zeftype": "Delegate", "order": z.order, "item": serialize_internal(z.item)}
     elif isinstance(z, pyinternals.DelegateTX):
         return {"_zeftype": "DelegateTX"}
     elif isinstance(z, pyinternals.DelegateRoot):
         return {"_zeftype": "DelegateRoot"}
     elif isinstance(z, pyinternals.DelegateRelationTriple):
-        return {"_zeftype": "DelegateRelationTriple", "rt": z.rt, "source": serialize_delegate(z.source), "target": serialize_delegate(z.target)}
+        return {"_zeftype": "DelegateRelationTriple", "rt": z.rt, "source": serialize_internal(z.source), "target": serialize_internal(z.target)}
 
     raise NotImplementedError(f"{z} not a delegate")
 
@@ -284,6 +277,15 @@ def serialize_zefops(k_type, ops):
 
     return {"_zeftype": k_type, "el_ops": serialized_ops}
 
+def serialize_valuetype(vt):
+    # Super dodgy version just to get something off the ground for now
+    return {
+        "_zeftype": "ValueType",
+        "type_name": vt.d["type_name"],
+        "absorbed": serialize_internal(vt.d["absorbed"])
+    }
+
+
 
 def deserialize_tuple(json_d: dict) -> tuple:
     return tuple(deserialize_internal(el) for el in json_d["items"])
@@ -413,19 +415,13 @@ def deserialize_zeftypes(z) -> dict:
 
 def deserialize_delegate(d) -> dict:
     if d['_zeftype'] == "Delegate":
-        return Delegate(d["order"], deserialize_delegate(d["item"]))
-    elif d['_zeftype'] == "DelegateEntity":
-        return pyinternals.DelegateEntity(d["et"])
-    elif d['_zeftype'] == "DelegateAtomicEntity":
-        return pyinternals.DelegateAtomicEntity(d["aet"])
-    elif d['_zeftype'] == "DelegateRelationGroup":
-        return pyinternals.DelegateRelationGroup(d["rt"])
+        return Delegate(d["order"], deserialize_internal(d["item"]))
     elif d['_zeftype'] == "DelegateTX":
         return pyinternals.DelegateTX()
     elif d['_zeftype'] == "DelegateRoot":
         return pyinternals.DelegateRoot()
     elif d['_zeftype'] == "DelegateRelationTriple":
-        return pyinternals.DelegateRelationTriple(d["rt"], deserialize_delegate(d["source"]), deserialize_delegate(d["target"]))
+        return pyinternals.DelegateRelationTriple(d["rt"], deserialize_internal(d["source"]), deserialize_internal(d["target"]))
 
     raise NotImplementedError(f"{d} not a serialized delegate")
 
@@ -445,6 +441,17 @@ def deserialize_zefops(ops):
 
     return deserialized_ops
 
+def deserialize_valuetype(d):
+    # Super dodgy version just to get something off the ground for now
+
+    # Look for the same typename
+    from . import VT
+    for var in dir(VT):
+        item = getattr(VT, var)
+        if isinstance(item, ValueType_) and item.d["type_name"] == d["type_name"]:
+            absorbed = deserialize_internal(d["absorbed"])
+            return ValueType_(type_name=d["type_name"], absorbed=absorbed)
+    raise Exception(f"Couldn't find a ValueType of type '{d['type_name']}'")
 
 serialization_mapping[ZefRef] = serialize_zeftypes
 # serialization_mapping[ZefRefs] = serialize_zeftypes
@@ -472,18 +479,15 @@ def deserialize_zefops(ops):
 serialization_mapping[AtomicEntity] = serialize_zeftypes
 serialization_mapping[_ErrorType] = serialize_zeftypes
 serialization_mapping[Image] = serialize_zeftypes
-serialization_mapping[_Effect_Class] = serialize_zeftypes
 serialization_mapping[FXElement] = serialize_zeftypes
 serialization_mapping[Delegate] = serialize_delegate
 serialization_mapping[FlatGraph] = serialize_flatgraph_or_flatref
 serialization_mapping[FlatRef] = serialize_flatgraph_or_flatref
 serialization_mapping[FlatRefs] = serialize_flatgraph_or_flatref
-serialization_mapping[pyinternals.DelegateEntity] = serialize_delegate
-serialization_mapping[pyinternals.DelegateAtomicEntity] = serialize_delegate
-serialization_mapping[pyinternals.DelegateRelationGroup] = serialize_delegate
 serialization_mapping[pyinternals.DelegateTX] = serialize_delegate
 serialization_mapping[pyinternals.DelegateRoot] = serialize_delegate
 serialization_mapping[pyinternals.DelegateRelationTriple] = serialize_delegate
+serialization_mapping[ValueType_] = serialize_valuetype
 
 
 deserialization_mapping["dict"] = deserialize_dict
@@ -518,9 +522,7 @@ def deserialize_zefops(ops):
 deserialization_mapping["FlatRef"] = deserialize_zeftypes
 deserialization_mapping["FlatRefs"] = deserialize_zeftypes
 deserialization_mapping["Delegate"] = deserialize_delegate
-deserialization_mapping["pyinternals.DelegateEntity"] = deserialize_delegate
-deserialization_mapping["pyinternals.DelegateAtomicEntity"] = deserialize_delegate
-deserialization_mapping["pyinternals.DelegateRelationGroup"] = deserialize_delegate
 deserialization_mapping["pyinternals.DelegateTX"] = deserialize_delegate
 deserialization_mapping["pyinternals.DelegateRoot"] = deserialize_delegate
 deserialization_mapping["pyinternals.DelegateRelationTriple"] = deserialize_delegate
+deserialization_mapping["ValueType"] = deserialize_valuetype
diff --git a/python/zef/core/zef_functions.py b/python/zef/core/zef_functions.py
index 74660da4..0fc7c94a 100644
--- a/python/zef/core/zef_functions.py
+++ b/python/zef/core/zef_functions.py
@@ -338,7 +338,7 @@ def make_function_entity(g, label, is_pure, func, **kwargs):
             z_python_str = z_zef_fct | Out[RT.PythonSourceCode] | collect
             # only assign a new value if the contents changed
             if (value(z_python_str)) != s:
-                z_python_str | assign_value[s] | g | run
+                z_python_str | assign[s] | g | run
 
             # If docstring already exists; update it may be
             if len(z_zef_fct | Outs[RT.DocString] |collect) == 1:
@@ -348,7 +348,7 @@ def make_function_entity(g, label, is_pure, func, **kwargs):
                     z_zef_fct | out_rel[RT.DocString] | terminate | g | run
                 # If Docstring was updated
                 elif docstring_maybe != (z_doctstring_str | value | collect):
-                    z_doctstring_str | assign_value[docstring_maybe] | collect
+                    z_doctstring_str | assign[docstring_maybe] | collect
             # Case where this function existed before introducting Docstring parsing
             else:
                 # If Docstring is defined attach it
@@ -392,12 +392,16 @@ def remove_binding(z_fct: ZefRef, name: str, z_attached_fct: EZefRef):
             z_ed | terminate | g | run
 
         def add_binding(z_fct: ZefRef, name: str, z_attached_fct):                    
-            z_rel = (instantiate(z_fct | to_ezefref | collect, RT.Binding, z_attached_fct | to_ezefref | collect, g) 
-                    | fill_or_attach[RT.Name, name] 
-                    | fill_or_attach[RT.UseTimeSlice, str(base_uid(z_attached_fct | frame | to_tx))]        # the zef function passed in by the user may be pointing to an earlier 
-                    | collect
-                    # TODO: this should directly point at the transaction once zefDB allows attaching RTs to txs. This is just a hack for now !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
-                    )
+            # z_rel = (instantiate(z_fct | to_ezefref | collect, RT.Binding, z_attached_fct | to_ezefref | collect, g) 
+            #         | fill_or_attach[RT.Name, name] 
+            #         | fill_or_attach[RT.UseTimeSlice, str(base_uid(z_attached_fct | frame | to_tx))]        # the zef function passed in by the user may be pointing to an earlier 
+            #         | collect
+            #         # TODO: this should directly point at the transaction once zefDB allows attaching RTs to txs. This is just a hack for now !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
+            #         )
+            z = instantiate(z_fct | to_ezefref | collect, RT.Binding, z_attached_fct | to_ezefref | collect, g) 
+            [z | fill_or_attach[RT.Name][name],
+             z | fill_or_attach[RT.UseTimeSlice][str(base_uid(z_attached_fct | frame | to_tx))]
+             ] | transact[g] | run
 
         list(bindings_to_remove.items()) | for_each[lambda p: remove_binding(z_zef_fct, *p)]
         list(bindings_to_add.items()) | for_each[lambda p: add_binding(z_zef_fct, *p)]
@@ -421,7 +425,7 @@ def __init__(self, func, *, is_pure=False):
         self.is_pure = is_pure
 
     def __getitem__(self, arg):
-        return call[self][arg]
+        return func[self][arg]
 
     def __call__(self, *args, **kwds):
         return self.func(*args, **kwds)
@@ -431,12 +435,12 @@ def __or__(self, other):
         # i.e: @func
         #      def f(x): pass
         # f | map
-        return call[self] | other
+        return func[self] | other
 
     def __ror__(self, other):
         # Similar to ror behavior but pipes the other way
         # i.e: map | f
-        return other | call[self]
+        return other | func[self]
     
         
 
diff --git a/python/zef/deprecated/tools/_others.py b/python/zef/deprecated/tools/_others.py
index 55910adf..1b8135e3 100644
--- a/python/zef/deprecated/tools/_others.py
+++ b/python/zef/deprecated/tools/_others.py
@@ -487,14 +487,15 @@ def extend_frontier(frontier: Set, rels: List[Dict]) -> Set:
 ##############################
 # * timeit
 #----------------------------
-def timeit(f):
+def timeit(f, runs=10000):
     """use as decorator on any fct for a very simple print output measuring the execution duration"""
     def wrapped(*args, **kwargs):
         import time
-        t0=time.time()
-        res = f(*args, **kwargs)
-        t1=time.time()
-        print(f"zefdb.timeit - duration for single run: {t1-t0} seconds")
+        t0=time.perf_counter()
+        for _ in range(runs):
+            res = f(*args, **kwargs)
+        t1=time.perf_counter()
+        print(f"zef.timeit - duration for {runs} runs: {t1-t0} seconds")
         return res
     return wrapped
 
@@ -502,7 +503,3 @@ def wrapped(*args, **kwargs):
 
 
 
-
-
-
-
diff --git a/python/zef/deprecated/tools/zascii.py b/python/zef/deprecated/tools/zascii.py
index 8026abb9..cc4c66db 100644
--- a/python/zef/deprecated/tools/zascii.py
+++ b/python/zef/deprecated/tools/zascii.py
@@ -442,7 +442,7 @@ def make_seed(s: List[str], coord: Tuple[int, int]):
           | ops.take_while[lambda d: d is not None] 
           | ops.collect 
           | ops.last 
-          | ops.insert['type', 'path_group']     # add the type key to the dict
+          | ops.insert['type']['path_group']     # add the type key to the dict
           | ops.collect 
         )
     return elements
diff --git a/python/zef/experimental/networkx.py b/python/zef/experimental/networkx.py
index 05b6943c..9ce0754a 100644
--- a/python/zef/experimental/networkx.py
+++ b/python/zef/experimental/networkx.py
@@ -22,53 +22,10 @@ class Direction(Enum):
     OUTGOING = 2
     INCOMING = 3
 
-# This class needs to tie in with ZefRef_to_node_ref
-class NodeRef:
-    def __init__(self, z):
-        assert isinstance(z, ZefRef)
-        self.z = z
-
-    def __repr__(self):
-        return f"Node(#{index(self.z)})"
-    def __eq__(self, other):
-        if not isinstance(other, NodeRef):
-            return NotImplemented
-        return self.z == other.z
-    def __hash__(self):
-        return hash(self.z)
-
-# This class needs to tie in with ZefRef_to_edge_ref
-# class EdgeRef:
-#     def __init__(self, z):
-#         assert isinstance(z, ZefRef)
-#         self.z = z
-
-#     def __repr__(self):
-#         src = source(self.z)
-#         trg = target(self.z)
-#         return f"Edge(#{index(src)} -> #{index(trg)})"
-
-#     def __eq__(self, other):
-#         if not isinstance(other, EdgeRef):
-#             return NotImplemented
-#         return self.z == other.z
-
-#     def __hash__(self):
-#         return hash(self.z)
-
-@func
-def ZefRef_to_node_ref(z):
-    # Here we provide a consistent "name" for a ZefRef to library functions that
-    # use the ProxyGraph. Even though we allow users to provide pretty much
-    # anything that fits into g[x], this consistency here should provide a
-    # better fit for algorithms that do comparisons, etc...
-    # return index(z)
-    return NodeRef(z)
-
 @func
 def ZefRef_to_edge_ref(z, direction):
-    src_ref = ZefRef_to_node_ref(source(z))
-    trg_ref = ZefRef_to_node_ref(target(z))
+    src_ref = source(z)
+    trg_ref = target(z)
     if direction == Direction.INCOMING:
         return (trg_ref, src_ref)
     elif direction == Direction.OUTGOING:
@@ -80,7 +37,6 @@ def ZefRef_to_edge_ref(z, direction):
             return (src_ref, trg_ref)
         else:
             return (trg_ref, src_ref)
-    # return EdgeRef(z)
 
 @func
 def all_edges(z, rt, direction):
@@ -123,9 +79,16 @@ def __init__(self,
         self.include_type_as_field = include_type_as_field
         assert not multigraph, "Currently no multigraph support"
 
+        if isinstance(self.node_filter, tuple):
+            raise Exception("Don't pass a tuple to ProxyGraph as the node_filter")
+
+        if isinstance(self.node_filter, list):
+            # This is a special case that I think will later not be needed
+            self.node_filter = set(self.node_filter)
+
         # On creation, we make a consistent mapping of indices, just so we can
         # be sure this won't change in the future.
-        self.node_mapping = self._all_node_refs()
+        # self.node_mapping = self._all_node_refs()
 
         if self.undirected:
             self.direction = Direction.BOTH
@@ -134,18 +97,19 @@ def __init__(self,
         else:
             self.direction = Direction.OUTGOING
         
-    def _all_node_refs(self):
-        if isinstance(self.node_filter, tuple):
-            raise Exception("Don't pass a tuple to ProxyGraph as the node_filter")
+    def _lazy_node_refs(self):
+        if type(self.node_filter) == set:
+            # This is an optimisation that will hopefully not be needed later on
+            return self.node_filter
+        else:
+            return self.gs | all[self.node_filter]
 
-        if isinstance(self.node_filter, list):
-            if isinstance(self.node_filter[0], NodeRef):
-                return self.node_filter
-            if isinstance(self.node_filter[0], ZefRef):
-                return [ZefRef_to_node_ref(x) for x in self.node_filter]
-            raise TypeError("Can't understand node_filter")
+    def _in_node_filter(self, node):
+        if type(self.node_filter) == set:
+            # This is an optimisation that will hopefully not be needed later on
+            return node in self.node_filter
         else:
-            return self.gs | all[self.node_filter] | func[list] | map[ZefRef_to_node_ref] | collect
+            return node | is_a[self.node_filter] | collect
 
     @property
     def nodes(self):
@@ -213,8 +177,9 @@ def _repr_pretty_(self, p, cycle):
 
     def number_of_nodes(self):
         return len(self.nodes)
-    def number_of_edges(self, u, v):
-        raise NotImplementedError("Need this for multigraphs")
+    def number_of_edges(self, u=None, v=None):
+        # raise NotImplementedError("Need this for multigraphs")
+        assert u is None and v is None, "TODO for multigraphs"
         return len(self.edges)
 
     def __getitem__(self, item):
@@ -223,8 +188,10 @@ def __getitem__(self, item):
 
         # Returns a view from a node which is the first half of an edge query.
         resolved = None
-        if isinstance(item, NodeRef):
-            resolved = item.z
+        if False:
+            pass
+        # elif isinstance(item, NodeRef):
+        #     resolved = item.z
         elif isinstance(item, (str,int)) or is_a(item, uid):
             resolved = self.gs[item]
         elif isinstance(item, ZefRef):
@@ -236,14 +203,13 @@ def __getitem__(self, item):
         return ProxyAtlasView(resolved, self)
 
     def __iter__(self):
-        return iter(self.node_mapping)
+        return iter(self._lazy_node_refs())
     def __len__(self):
-        return len(self.node_mapping)
+        return length(self._lazy_node_refs())
     def nbunch_iter(self, nbunch=None):
         if nbunch is None:
             return iter(self)
-        nbunch = [ZefRef_to_node_ref(self[x].z) for x in nbunch]
-        return iter(set(nbunch) & set(self.node_mapping))
+        return (z for z in set(nbunch) if self._in_node_filter(x))
         
 
     # Graph properties
@@ -256,11 +222,15 @@ def is_multigraph(self):
     # TODO:
     # has_edge
     # adjacency
+    # size
+
     def order(self):
         return self.number_of_nodes()
-    # size
-    # subgraph - this could be done with an explicit node list, rather than ets
+
     def subgraph(self, nodes):
+        if type(nodes) != set:
+            raise Exception("Subgraph requires a set. In the future it will be possible to consider the union of query types.")
+
         return ProxyGraph(self.gs,
                            nodes,
                            self.rts,
@@ -272,7 +242,7 @@ def subgraph(self, nodes):
                            
     def reverse(self):
         return ProxyGraph(self.gs,
-                           self.node_mapping,
+                           self.node_filter,
                            self.rts,
                            undirected=self.undirected,
                            multigraph=self.multigraph,
@@ -282,7 +252,7 @@ def reverse(self):
 
     def to_undirected(self):
         return ProxyGraph(self.gs,
-                           self.node_mapping,
+                           self.node_filter,
                            self.rts,
                            undirected=True,
                            multigraph=self.multigraph,
@@ -292,7 +262,7 @@ def to_undirected(self):
 
     def to_directed(self):
         return ProxyGraph(self.gs,
-                           self.node_mapping,
+                           self.node_filter,
                            self.rts,
                            undirected=False,
                            multigraph=self.multigraph,
@@ -339,9 +309,9 @@ def __getitem__(self, node):
     #     raise NotImplementedError()
 
     def __iter__(self):
-        return iter(self.dg.node_mapping)
+        return iter(self.dg)
     def __len__(self):
-        return len(self.dg.node_mapping)
+        return len(self.dg)
 
     def items(self):
         for n in self:
@@ -401,11 +371,10 @@ def __iter__(self):
             direction_override = Direction.OUTGOING
         else:
             direction_override = self.dg.direction
-        out = (self.dg.node_mapping
-                 | map[lambda x: x.z]
-                 | map[all_edges_with_end[self.dg.rts][direction_override]]
-                 | concat
-                 | filter[second | func[ZefRef_to_node_ref] | contained_in[self.dg.node_mapping]]
+
+        out = (LazyValue(self.dg._lazy_node_refs())
+                 | map_cat[all_edges_with_end[self.dg.rts][direction_override]]
+                 | filter[second | func[self.dg._in_node_filter]]
                  | map[first]
                  | map[ZefRef_to_edge_ref[self.dg.direction]])
         if self.dataview:
@@ -462,17 +431,21 @@ def __init__(self, z : ZefRef, dg : ProxyGraph):
         self.dg = dg
 
     def __repr__(self):
-        return f"AtlasView({ZefRef_to_node_ref(self.z)})"
+        return f"AtlasView({self.z})"
     def _repr_pretty_(self, p, cycle):
         p.text(str(self))
 
     def __iter__(self):
         # This returns the neighbors, not the edges
         # all_edges_with_end returns a tuple with (rel,ent)
-        lazy = self.z | all_edges_with_end[self.dg.rts][self.dg.direction] | map[second] | map[ZefRef_to_node_ref] | filter[contained_in[self.dg.node_mapping]]
+        lazy = self.z | all_edges_with_end[self.dg.rts][self.dg.direction] | map[second] | filter[self.dg._in_node_filter]
         return iter(lazy)
     def __len__(self):
         return length(iter(self))
+    def keys(self):
+        return iter(self)
+    def items(self):
+        return ((x, self[x]) for x in self.keys())
 
     def __getitem__(self, other):
         # The other is a node itself, so we need to look it up and then find the
diff --git a/python/zef/gql/__init__.py b/python/zef/gql/__init__.py
index 903df568..84b3a2c4 100644
--- a/python/zef/gql/__init__.py
+++ b/python/zef/gql/__init__.py
@@ -14,7 +14,6 @@
 
 from ..core import *
 from ..ops import *
-from typing import *
 from .auto import auto_generate_gql
 from ..core.logger import log
 
@@ -84,7 +83,7 @@ def gql_enums(schema_node: ZefRef):
 @func
 def gql_types_dict(schema_node: ZefRef) -> dict:
     schema_node_checks(schema_node)
-    return {str(t >> RT.Name | value | collect) : t for t in schema_node | outs | target | filter[lambda x: x | BT | collect == BT.ENTITY_NODE] | collect}
+    return {str(t >> RT.Name | value | collect) : t for t in schema_node | Outs[RT]  | filter[lambda x: x | BT | collect == BT.ENTITY_NODE] | collect}
 
 @func
 def gql_fields_rt_dict(gql_type: ZefRef) -> dict:
@@ -95,7 +94,7 @@ def gql_fields_rt_dict(gql_type: ZefRef) -> dict:
 def gql_field_resolver(gql_field: ZefRef) -> ZefRef:
     gql_field_checks(gql_field)
     resolver_types = [RT.GQL_Resolve_with, RT.GQL_Resolve_with_intermediate, RT.GQL_Resolve_with_body, RT.GQL_Resolve_with_zcript]
-    resolvers = gql_field | outs | filter[lambda z: z | RT in resolver_types] | collect
+    resolvers = gql_field | Outs[RT] | filter[lambda z: z | RT in resolver_types] | collect
     if len(resolvers) != 1:
         raise Exception(f"There wasn't exactly one gql_resolver associated with field {gql_field}")
     return resolvers | only | collect
diff --git a/python/zef/gql/ariadne_utils.py b/python/zef/gql/ariadne_utils.py
index e9a51f4f..17c8728e 100644
--- a/python/zef/gql/ariadne_utils.py
+++ b/python/zef/gql/ariadne_utils.py
@@ -49,7 +49,7 @@ def gqlify(s):
 # data utils
 def parse_list_et_gql(list_et):
     aet_values = []
-    arguments = list_et >> L[RT.argument] | collect
+    arguments = list_et | Outs[RT.argument] | collect
     for arg in arguments:
         if BT(arg) == BT.ENTITY_NODE:
             return ""
@@ -66,13 +66,13 @@ def parse_list_et_gql(list_et):
 
 # ariadne utils
 def handle_params(rt, params):
-    if length(rt >> L[RT(gqlify("queryParams"))]) > 0:
-        for param in rt > L[RT(gqlify("queryParams"))]:
-            param_name = de_gqlify(str(param >> RT.Name | value | collect), False)
+    if length(rt | Outs[RT(gqlify("queryParams"))]) > 0:
+        for param in rt | out_rels[RT(gqlify("queryParams"))]:
+            param_name = de_gqlify(str(param | Out[ RT.Name] | value | collect), False)
             if BT(param | target | collect) == BT.ATOMIC_ENTITY_NODE:
                 aet_type = str(AET(param | target | collect))
                 aet_value = param | target | value | collect
-                if length(param >> L[RT(gqlify("nonNullable"))]) > 0:
+                if length(param | Outs[RT(gqlify("nonNullable"))]) > 0:
                     params.append(f"{param_name}")
                 else:
                     params.append(f"{param_name} = {aet_value}")
@@ -86,38 +86,39 @@ def handle_params(rt, params):
 
 
 def resolve_with(rt, bt, ft):
-    d_rt = rt >> RT(gqlify("resolve_with")) | collect
+    optional = single_or[None]
+    d_rt = rt | Out[RT(gqlify("resolve_with"))] | collect
     d_rt_name = str(RT(d_rt))
     is_list = is_a(ft, ET(gqlify("list")))
     if is_list:
-        bt = BT(ft >> RT.argument | to_ezefref | collect)
-    is_out = rt > RT(gqlify("resolve_with")) >> O[RT.IsOut] | value_or[True] | collect
+        bt = BT(ft | Out[RT.argument] | to_ezefref | collect)
+    is_out = rt | out_rel[RT(gqlify("resolve_with"))] | Outs[RT.IsOut] | optional | value_or[True] | collect
 
-    dir = ">>" if is_out else "<<"
+    dir = "Outs" if is_out else "Ins"
 
     if bt == BT.ATOMIC_ENTITY_NODE:
         if is_list:
-            return f'return (z {dir} L[RT.{d_rt_name}]) | map[value] | collect'
+            return f'return (z | {dir}[RT.{d_rt_name}]) | map[value] | collect'
         else:
-            return f'return (z {dir} O[RT.{d_rt_name}]) | maybe_value | collect'
+            return f'return (z | {dir}[RT.{d_rt_name}]) | optional | maybe_value | collect'
     else:
         if is_list:
             # return f'return (z >> L[RT.{d_rt_name}] | collect) , (z > L[RT.{d_rt_name}])| map[uid] | collect'
-            return f'return (z {dir} L[RT.{d_rt_name}] | collect)'
+            return f'return (z | {dir}[RT.{d_rt_name}] | collect)'
         else:
             # return f'return (z >> RT.{d_rt_name} | collect) , (z > RT.{d_rt_name})|uid | collect'
-            return f'return (z {dir} O[RT.{d_rt_name}] | collect)'
+            return f'return (z | {dir}[RT.{d_rt_name}] | optional | collect)'
 
 
 def resolve_with_script(rt):
-    zef_script = rt >> RT(gqlify("resolve_with_script"))
+    zef_script = rt | Out[ RT(gqlify("resolve_with_script"))]
     script = f'g["{zef_script | uid | collect}"] | now | collect'
     return f'zef_script = {script} ; return zef_execute(zef_script, z=z)'
 
 def resolve_with_func(rt):
-    z_func = rt >> RT(gqlify("resolve_with_func"))
+    z_func = rt | Out[ RT(gqlify("resolve_with_func"))]
 
-    extra = rt >> L[RT(gqlify("queryParams"))] >> RT.Name | value | map[lambda s: de_gqlify(s,False)] | collect
+    extra = rt | Outs[RT(gqlify("queryParams"))] | Out[ RT.Name] | value | map[lambda s: de_gqlify(s,False)] | collect
     param_names = {"z", "ctx", "g", *extra}
 
     dict_create = '{' + ', '.join(f"'{key}': {key}" for key in param_names) + '}'
@@ -128,14 +129,14 @@ def resolve_with_func(rt):
     '''
 
 def resolve_with_intermediate(rt):
-    d_rt = rt >> RT(gqlify("resolve_with_intermediate"))
+    d_rt = rt | Out[ RT(gqlify("resolve_with_intermediate"))]
     d_rt_name = str(RT(d_rt))
-    return f'return (z > RT.{d_rt_name}), (z > RT.{d_rt_name})|uid | collect'
+    return f'return (z | out_rel[RT.{d_rt_name})], (z | out_rel[RT.{d_rt_name})] |uid | collect'
 
 
 def resolve_with_zef_function(rt):
-    zef_function_uid = str(rt >> RT(gqlify("resolve_with_zef_function")) | uid | collect)
-    params = rt > L[RT(gqlify("queryParams"))] >> RT.Name | value | map[lambda s: de_gqlify(s,False)] | collect
+    zef_function_uid = str(rt | Out[ RT(gqlify("resolve_with_zef_function"))] | uid | collect)
+    params = rt | out_rels[RT(gqlify("queryParams"))] | map[Out[ RT.Name] | value] | map[lambda s: de_gqlify(s,False)] | collect
     defaults = ', '.join(f"{key} = {key}" for key in {"z", "ctx", "g"})
     params = ', '.join(i for i in params) + "," if len(params) != 0 else ""
     return f'return g["{zef_function_uid}"]({params}{defaults} )'
@@ -158,24 +159,24 @@ def create_function_body(ot, ft, bt, rt, fn, fallback_resolvers):
     params = handle_params(rt, default_params)
     # TODO (Cleanup) might consider making these as well customer specific
     # Resolve_With relations have highest priority
-    if length(rt >> L[RT(gqlify("resolve_with"))]) > 0:
+    if length(rt | Outs[RT(gqlify("resolve_with"))]) > 0:
         return resolve_with_wrapper(resolve_with(rt, bt, ft)), params
 
-    elif length(rt >> L[RT(gqlify("resolve_with_script"))]) > 0:
+    elif length(rt | Outs[RT(gqlify("resolve_with_script"))]) > 0:
         return resolve_with_wrapper(resolve_with_script(rt)), params
 
-    elif length(rt >> L[RT(gqlify("resolve_with_zef_function"))]) > 0:
+    elif length(rt | Outs[RT(gqlify("resolve_with_zef_function"))]) > 0:
         return resolve_with_wrapper(resolve_with_zef_function(rt)), params
 
-    elif length(rt >> L[RT(gqlify("resolve_with_func"))]) > 0:
+    elif length(rt | Outs[RT(gqlify("resolve_with_func"))]) > 0:
         return resolve_with_wrapper(resolve_with_func(rt)), params
 
-    elif length(rt >> L[RT(gqlify("resolve_with_intermediate"))]) > 0:
+    elif length(rt | Outs[RT(gqlify("resolve_with_intermediate"))]) > 0:
         return resolve_with_wrapper(resolve_with_intermediate(rt)), params
 
-    elif length(rt >> L[RT(gqlify("resolve_with_body"))]) > 0:
-        # return (rt >> RT(gqlify("resolve_with_body")) | value | collect), params
-        return resolve_with_wrapper(rt >> RT(gqlify("resolve_with_body")) | value | collect), params
+    elif length(rt | Outs[RT(gqlify("resolve_with_body"))]) > 0:
+        # return (rt | Out[ RT(gqlify("resolve_with_body")) | value | collect), params
+        return resolve_with_wrapper(rt | Out[ RT(gqlify("resolve_with_body"))] | value | collect), params
 
     return fallback_resolvers(ot, ft, bt, rt, fn), default_params
 
@@ -199,10 +200,10 @@ def custom_sort(item):
 
 
 def generate_enum_resolver(e, cog):
-    enum_name = de_gqlify(str(e >> RT.Name | value | collect), True)
+    enum_name = de_gqlify(str(e | Out[ RT.Name ]| value | collect), True)
     cog.outl(f'{enum_name} = EnumType("{enum_name}", {"{"}')
     i = -1
-    for f in (e >> L[RT(gqlify("field"))]):
+    for f in (e | Outs[RT(gqlify("field"))]):
         i += 1
         en_value = (f | value).enum_value
 
@@ -212,13 +213,13 @@ def generate_enum_resolver(e, cog):
 
 
 def generate_interface_resolver(i, cog):
-    interface_name = de_gqlify(str(i >> RT.Name | value | collect), True)
+    interface_name = de_gqlify(str(i | Out[ RT.Name ]| value | collect), True)
     cog.outl(f'{interface_name} = InterfaceType("{interface_name}")')
     cog.outl(f'@{interface_name}.type_resolver')
     cog.outl(f'def resolve_{interface_name}_type_resolver(obj, *_):')
     cog.outl(f'#TODO make sure that those names are consistent with your Data Graph type name')
-    for t in (i << L[RT(gqlify("implements"))]):
-        object_name = de_gqlify(str(t >> RT.Name | value | collect), True)
+    for t in (i |  Ins[RT(gqlify("implements"))]):
+        object_name = de_gqlify(str(t | Out[ RT.Name ]| value | collect), True)
         cog.outl(f'    if str(ET(obj)) == "{object_name}":')
         cog.outl(f'        return "{object_name}"')
     cog.outl(f'    if str(ET(obj)) == "Step":')
@@ -232,19 +233,19 @@ def generate_all(global_dict, cog):
     default_resolvers_list = global_dict["default_resolvers_list"]
     fallback_resolvers = global_dict["fallback_resolvers"]
     object_types = []
-    types = root >> L[RT(gqlify("type"))]
-    interfaces = root >> L[RT(gqlify("interface"))]
+    types = root | Outs[RT(gqlify("type"))]
+    interfaces = root | Outs[RT(gqlify("interface"))]
 
     for t in types:
-        object_type = de_gqlify(str(t >> RT.Name | value | collect), True)
+        object_type = de_gqlify(str(t | Out[ RT.Name ]| value | collect), True)
         # Don't generate resolvers for function in this list
         if object_type in default_resolvers_list:
             continue
         object_types.append(object_type)
         initialize_object_type(object_type, cog)
 
-        for rt in (t > L[RT(gqlify("field"))]):
-            field_name = de_gqlify(str(rt >> RT.Name | value | collect), False)
+        for rt in (t | out_rels[RT(gqlify("field"))]):
+            field_name = de_gqlify(str(rt | Out[ RT.Name] | value | collect), False)
             field_type = (rt | target | to_ezefref | collect)
             fn_body, params = create_function_body(object_type, field_type, BT(field_type), rt, field_name, fallback_resolvers)
             generate_object_resolver(object_type, field_name, rt, fn_body, params, cog)
diff --git a/python/zef/gql/auto.py b/python/zef/gql/auto.py
index 79d96857..e3f06d98 100644
--- a/python/zef/gql/auto.py
+++ b/python/zef/gql/auto.py
@@ -41,9 +41,9 @@ def del_is_a(x, T):
         return x | And[is_a[Delegate]][abstract_type | is_a[T]] | collect
         
     for ent in g | all | filter[del_is_a[ET]] | collect:
-        fields = ent > L[RT] | filter[lambda z: z | target | del_is_a[AET] | collect] | collect
-        out_others = ent > L[RT] | filter[lambda z: z | target | del_is_a[ET] | collect] | collect
-        in_others = ent < L[RT] | filter[lambda z: z | source | del_is_a[ET] | collect] | collect
+        fields = ent | out_rels[RT] | filter[target | del_is_a[AET] | collect] | collect
+        out_others = ent | out_rels[RT] | filter[target | del_is_a[ET] | collect] | collect
+        in_others = ent | in_rels[RT] | filter[source | del_is_a[ET] | collect] | collect
         if len(fields) == 0 and len(out_others) == 0 and len(in_others) == 0:
             continue
         name = "GQL_" + str(ET(ent))
@@ -52,10 +52,15 @@ def del_is_a(x, T):
             (Z[name], RT.Name, name),
         ]
 
-        all_rts = (fields + out_others) | map[RT] | collect
-        dup_rts = all_rts | filter[lambda x: all_rts.count(x) > 1] | collect
-        if len(dup_rts) > 0:
-            raise Exception(f"Can't auto-generate GQL, there are multiple RTs {dup_rts} which need to be disambiguated")
+        all_out_rts = (fields + out_others) | map[RT] | collect
+        dup_out_rts = all_out_rts | group_by[identity] | filter[second | length | greater_than_or_equal[2]] | map[first] | collect
+        if len(dup_out_rts) > 0:
+            raise Exception(f"Can't auto-generate GQL, there are multiple outgoing RTs {dup_out_rts!r} on type {ET(ent)!r} which need to be disambiguated")
+
+        all_in_rts = in_others | map[RT] | collect
+        dup_in_rts = all_in_rts | group_by[identity] | filter[second | length | greater_than_or_equal[2]] | map[first] | collect
+        if len(dup_in_rts) > 0:
+            raise Exception(f"Can't auto-generate GQL, there are multiple incoming RTs {dup_in_rts!r} on type {ET(ent)!r} which need to be disambiguated")
 
         for field in fields:
             f_name = "GQL_" + str(RT(field))
diff --git a/python/zef/gql/generate_gql_api.py b/python/zef/gql/generate_gql_api.py
index d433a3c4..1410dba3 100644
--- a/python/zef/gql/generate_gql_api.py
+++ b/python/zef/gql/generate_gql_api.py
@@ -31,15 +31,16 @@ def generate_resolvers_fcts(schema_root, resolvers_destination):
     from cogapp import Cog
     cog = Cog()
     cog.options.bReplace = True
+    optional = single_or[None]
 
-    z = schema_root >> O[RT.DefaultResolversList] | maybe_value | collect
+    z = schema_root | Outs[RT.DefaultResolversList] | optional | maybe_value | collect
     if z is None:
         default_resolvers_list = []
     else:
         default_resolvers_list = from_json(z)
         
         
-    z = schema_root >> O[RT.FallbackResolvers] | collect
+    z = schema_root | Outs[RT.FallbackResolvers] | optional | collect
     if z is None:
         fallback_resolvers = lambda *args,**kwds: ("return None #This means that no resolver is defined!", ["z", "ctx"])
     else:
@@ -48,7 +49,7 @@ def generate_resolvers_fcts(schema_root, resolvers_destination):
         exec(fallback_resolvers, d)
         fallback_resolvers = d["fallback_resolvers"]
 
-    additional_exec = schema_root >> O[RT.AdditionalExec] | value_or[""] | collect
+    additional_exec = schema_root | Outs[RT.AdditionalExec] | optional | value_or[""] | collect
 
     global_dict = {"schema_root": schema_root,
                    "default_resolvers_list": default_resolvers_list,
diff --git a/python/zef/gql/schema_graph_to_file.py b/python/zef/gql/schema_graph_to_file.py
index 699c0e65..2a958667 100644
--- a/python/zef/gql/schema_graph_to_file.py
+++ b/python/zef/gql/schema_graph_to_file.py
@@ -19,11 +19,11 @@
 
 def parse_list_et(list_et):
     aet_values = []
-    arguments = list_et >> L[RT.argument] | collect
+    arguments = list_et | Outs[RT.argument] | collect
     at = ""
     for arg in arguments:
         if BT(arg) == BT.ENTITY_NODE:
-            return "", de_gqlify(value(arg >> RT.Name), True)
+            return "", de_gqlify(value(arg | Out[ RT.Name]), True)
         else:
             aet_type = str(AET(arg | to_ezefref | collect))
             at = aet_type
@@ -40,24 +40,24 @@ def parse_list_et(list_et):
 
 def check_nonnullable(param):
     nonnullable = ""
-    if length(param > L[RT(gqlify("nonNullable"))]) > 0:
+    if length(param | out_rels[RT(gqlify("nonNullable"))]) > 0:
         nonnullable = "!"
     return nonnullable
 
 
 def check_implements(param):
     implements = ""
-    if length(param > L[RT(gqlify("implements"))]) > 0:
-        interface_name = de_gqlify(value(param >> RT(gqlify("implements")) >> RT.Name), True)
+    if length(param | out_rels[RT(gqlify("implements"))]) > 0:
+        interface_name = de_gqlify(value(param | Out[ RT(gqlify("implements"))] | Out[ RT.Name]), True)
         implements = f"implements {interface_name}"
     return implements
 
 
 def parse_queryparams_et(params_et):
     returned_list = []
-    if length(params_et >> L[RT(gqlify("queryParams"))]) > 0:
-        for param in params_et > L[RT(gqlify("queryParams"))] | collect:
-            param_name = de_gqlify(value(param >> RT.Name), False)
+    if length(params_et | Outs[RT(gqlify("queryParams"))]) > 0:
+        for param in params_et | out_rels[RT(gqlify("queryParams"))] | collect:
+            param_name = de_gqlify(value(param | Out[ RT.Name]), False)
             nonnullable = check_nonnullable(param)
             if BT(param | target | collect) == BT.ATOMIC_ENTITY_NODE:
                 aet_type = str(AET(param | target | collect))
@@ -75,7 +75,7 @@ def parse_queryparams_et(params_et):
                         aet_values = " = " + aet_values
                     returned_list.append(f"{param_name}: [{et_type}]{nonnullable}{aet_values}")
                 else:
-                    et_type = de_gqlify(value(((param | target) >> RT.Name)), True)
+                    et_type = de_gqlify(value(((param | target) | Out[ RT.Name])), True)
                     returned_list.append(f"{param_name}: {et_type}{nonnullable}")
 
         return returned_list
@@ -84,9 +84,9 @@ def parse_queryparams_et(params_et):
 
 
 def generate_fields(et, schema_out):
-    for rt in (et > L[RT(gqlify("field"))] | collect):
+    for rt in (et | out_rels[RT(gqlify("field"))] | collect):
         nonnullable = check_nonnullable(rt)
-        field_name = de_gqlify(value(rt >> RT.Name), False)
+        field_name = de_gqlify(value(rt | Out[ RT.Name]), False)
         field_return = rt | target | collect
         blob_type = BT(field_return | to_ezefref | collect)
         query_params = parse_queryparams_et(rt)
@@ -115,13 +115,13 @@ def inject_params(qp):
                     aet_values = " = " + aet_values
                 schema_out += f'  {field_name}{inject_params(query_params)}: [{et_type}]{nonnullable}{aet_values}\n'
             else:
-                schema_out += f'  {field_name}{inject_params(query_params)}: {de_gqlify(value(field_return >> RT.Name),True)}{nonnullable}\n'
+                schema_out += f'  {field_name}{inject_params(query_params)}: {de_gqlify(value(field_return | Out[ RT.Name]),True)}{nonnullable}\n'
     schema_out += "}\n"
     return schema_out
 
 
 def generate_enum_values(en, schema_out):
-    for field in (en >> L[RT(gqlify("field"))] | collect):
+    for field in (en | Outs[RT(gqlify("field"))] | collect):
         en_value = (field | value).enum_value
         schema_out += f'  {en_value}\n'
     schema_out += "}\n"
@@ -129,35 +129,35 @@ def generate_enum_values(en, schema_out):
 
 
 def generate_api_schema_string(schema_root):
-    types = schema_root >> L[RT(gqlify("type"))] | collect
-    inputs = schema_root >> L[RT(gqlify("input"))] | collect
-    interfaces = schema_root >> L[RT(gqlify("interface"))] | collect
-    scalars = schema_root >> L[RT(gqlify("scalar"))] | collect
-    enums = schema_root >> L[RT(gqlify("enum"))] | collect
+    types = schema_root | Outs[RT(gqlify("type"))] | collect
+    inputs = schema_root | Outs[RT(gqlify("input"))] | collect
+    interfaces = schema_root | Outs[RT(gqlify("interface"))] | collect
+    scalars = schema_root | Outs[RT(gqlify("scalar"))] | collect
+    enums = schema_root | Outs[RT(gqlify("enum"))] | collect
 
     schema_out = ""
     for et in types:
         implements = check_implements(et)
-        object_type = de_gqlify(value(et >> RT.Name), True)
+        object_type = de_gqlify(value(et | Out[ RT.Name]), True)
         schema_out += f'type {object_type} {implements} {"{"}\n'
         schema_out = generate_fields(et, schema_out)
 
     for et in inputs:
-        object_type = de_gqlify(value(et >> RT.Name), True)
+        object_type = de_gqlify(value(et | Out[ RT.Name]), True)
         schema_out += f'input {object_type} {"{"}\n'
         schema_out = generate_fields(et, schema_out)
 
     for et in interfaces:
-        object_type = de_gqlify(value(et >> RT.Name), True)
+        object_type = de_gqlify(value(et | Out[ RT.Name]), True)
         schema_out += f'interface {object_type} {"{"}\n'
         schema_out = generate_fields(et, schema_out)
 
     for et in scalars:
-        object_type = de_gqlify(value(et >> RT.Name), True)
+        object_type = de_gqlify(value(et | Out[ RT.Name]), True)
         schema_out += f'scalar {object_type}\n'
 
     for et in enums:
-        object_type = de_gqlify(value(et >> RT.Name), True)
+        object_type = de_gqlify(value(et | Out[ RT.Name]), True)
         schema_out += f'enum {object_type} {"{"}\n'
         schema_out = generate_enum_values(et, schema_out)
 
diff --git a/python/zef/gql/simplegql/__main__.py b/python/zef/gql/simplegql/__main__.py
index 6d96f341..e2c85e11 100644
--- a/python/zef/gql/simplegql/__main__.py
+++ b/python/zef/gql/simplegql/__main__.py
@@ -14,33 +14,40 @@
 
 from ... import *
 from ...ops import *
+from .main import create_schema_graph
 from .server import start_server
 
 from zef.core.logger import log
 
+import os
 import argparse
 parser = argparse.ArgumentParser()
-parser.add_argument("schema_file", type=str, metavar="SCHEMA-FILE",
+parser.add_argument("--schema-file", type=str, metavar="schema_file", default=os.environ.get("SIMPLEGQL_SCHEMA_FILE", None),
                     help="The graphql schema file.")
-parser.add_argument("data_tag", type=str, metavar="DATA-TAG",
+parser.add_argument("--data-tag", type=str, metavar="data_tag", default=os.environ.get("SIMPLEGQL_DATA_TAG"),
                     help="The tag of the Zef graph with the data (not the schema).")
-parser.add_argument("--port", type=int, default=443)
-parser.add_argument("--bind", type=str, default="0.0.0.0",
+parser.add_argument("--port", type=int, default=int(os.environ.get("SIMPLEGQL_PORT", "443")))
+parser.add_argument("--bind", type=str, default=os.environ.get("SIMPLEGQL_BIND", "0.0.0.0"),
                     help="IP address to bind to.")
-parser.add_argument("--no-host-role", action="store_false", dest="host_role",
+parser.add_argument("--no-host-role", action="store_false", dest="host_role", default=(False if "SIMPLEGQL_NO_HOST_ROLE" in os.environ else True),
                     help="Whether the server should acquire host role on the data graph.")
-parser.add_argument("--create", action="store_true",
+parser.add_argument("--create", action="store_true", default=(True if "SIMPLEGQL_CREATE" in os.environ else None),
                     help="If the data graph does not exist, then create a new blank graph.")
+parser.add_argument("--hooks", type=str, dest="hooks_file", default=os.environ.get("SIMPLEGQL_HOOKS_FILE", None),
+                    help="If present, a python file containing the hooks to make available for the schema file")
+parser.add_argument("--init-hook", type=str, dest="init_hook", default=os.environ.get("SIMPLEGQL_INIT_HOOK", None),
+                    help="If present, a function in the hooks file to run on initialisation of the data graph")
 args = parser.parse_args()
 
-from .schema_file_parser import parse_partial_graphql, json_to_minimal_nodes
 
-g_schema = Graph()
 schema_gql = args.schema_file | read_file | run | get["content"] | collect
-parsed_dict = parse_partial_graphql(schema_gql)
-commands = json_to_minimal_nodes(parsed_dict)
-r = commands | transact[g_schema] | run
-root = r["root"]
+if args.hooks_file is not None:
+    hooks_string = args.hooks_file | read_file | run | get["content"] | collect
+    log.info(f"Going to read hooks from '{args.hooks_file}'")
+else:
+    hooks_string = None
+
+root = create_schema_graph(schema_gql, hooks_string)
 log.info(f"Created schema graph from '{args.schema_file}'")
 
 guid = lookup_uid(args.data_tag)
@@ -60,9 +67,24 @@
     log.info("Loaded existing data graph")
 
 if args.host_role:
-    make_primary(g_data)
+    try:
+        g_data | take_transactor_role | run
+    except:
+        print("""
+Unable to obtain host role for data graph. Either stop other processes from having host role on this graph (`g | release_transactor_role | run` in that process) or run this server with `--no-host-role`).
+        
+Note that running without the host role is currently dangerous as mutations do not currently verify pre-conditions.
+""") 
+        raise SystemExit(3)
     log.info("Obtained host role on data graph")
 
+if args.init_hook is not None:
+    g_schema = Graph(root)
+    hook_func = g_schema | now | get[args.init_hook] | collect
+    hook_func(g_data)
+    log.info(f"Ran init hook: {args.init_hook}")
+    
+
 server_uuid = start_server(root, g_data, args.port, args.bind)
 
 import time
diff --git a/python/zef/gql/simplegql/generate_api.py b/python/zef/gql/simplegql/generate_api.py
index 3da6a53c..7fccbbe2 100644
--- a/python/zef/gql/simplegql/generate_api.py
+++ b/python/zef/gql/simplegql/generate_api.py
@@ -33,37 +33,41 @@ def is_core_scalar(z):
         is_a(z, AET.Bool)
     )
     
-assert_type = Assert[Or[is_a[ET.GQL_Type]][is_a[ET.GQL_Enum]][is_a[AET]]][lambda z: f"{z} is not a GQL type"]
+assert_type = Assert[is_a[ET.GQL_Type | ET.GQL_Enum | AET]][lambda z: f"{z} is not a GQL type"]
 assert_field = Assert[is_a[RT.GQL_Field]][lambda z: f"{z} is not a GQL field"]
 
+optional = single_or[None]
+
 @func
-def single_or(itr, default):
-    itr = iter(itr)
-    try:
-        ret = next(itr)
-        try:
-            next(itr)
-            raise Exception("single_or detected more than one item in iterator")
-        except StopIteration:
-            return ret
-    except StopIteration:
-        return default
+def OutO(z, rt):
+    if z is None:
+        return None
+    return z | Outs[rt] | optional | collect
+
+@func
+def fvalue(z, rt, *args):
+    if len(args) == 0:
+        return z | Out[rt] | value | collect
+    else:
+        default, = args
+        return z | OutO[rt] | value_or[default] | collect
+
         
-op_is_scalar = assert_type | Or[is_a[AET]][is_a[ET.GQL_Enum]]
-op_is_orderable = assert_type | Or[is_a[AET.Float]][is_a[AET.Int]][is_a[AET.Time]]
-op_is_summable = assert_type | Or[is_a[AET.Float]][is_a[AET.Int]]
+op_is_scalar = assert_type | is_a[AET | ET.GQL_Enum]
+op_is_orderable = assert_type | is_a[AET.Float | AET.Int | AET.Time]
+op_is_summable = assert_type | is_a[AET.Float | AET.Int]
 op_is_stringlike = assert_type | is_a[AET.String]
-op_is_list = assert_field >> O[RT.List] | value_or[False] | collect
-op_is_required = assert_field >> O[RT.Required] | value_or[False] | collect
-op_is_unique = assert_field >> O[RT.Unique] | value_or[False] | collect
-op_is_searchable = assert_field >> O[RT.Search] | value_or[False] | collect
+op_is_list = assert_field | fvalue[RT.List][False] | collect
+op_is_required = assert_field | fvalue[RT.Required][False] | collect
+op_is_unique = assert_field | fvalue[RT.Unique][False] | collect
+op_is_searchable = assert_field | fvalue[RT.Search][False] | collect
 op_is_aggregable = assert_field | And[Not[op_is_list]][target | Or[op_is_orderable][op_is_summable]]
-op_is_incoming = assert_field >> O[RT.Incoming] | value_or[False] | collect
+op_is_incoming = assert_field | fvalue[RT.Incoming][False] | collect
 
-op_is_upfetch = assert_field >> O[RT.Upfetch] | value_or[False] | collect
-op_upfetch_field = (assert_type > L[RT.GQL_Field]
+op_is_upfetch = assert_field | fvalue[RT.Upfetch][False] | collect
+op_upfetch_field = (assert_type | out_rels[RT.GQL_Field]
                     | filter[op_is_upfetch]
-                    | single_or[None]
+                    | optional
                     | collect)
 op_has_upfetch = op_upfetch_field | Not[equals[None]] | collect
 
@@ -90,8 +94,8 @@ def generate_resolvers_fcts(schema_root):
     TimeScalar.set_serializer(lambda t: datetime.datetime.fromtimestamp(t.seconds_since_1970).isoformat())
     TimeScalar.set_value_parser(Time)
 
-    for z_type in schema_root >> L[RT.GQL_Type]:
-        name = value(z_type >> RT.Name)
+    for z_type in schema_root | Outs[RT.GQL_Type]:
+        name = z_type | F.Name | collect
         Type = ObjectType(name)
         all_objects += [Type]
 
@@ -101,8 +105,8 @@ def generate_resolvers_fcts(schema_root):
         upfetch_field_schemas = []
         aggregate_outs = []
 
-        for z_field in z_type > L[RT.GQL_Field]:
-            field_name = z_field >> RT.Name | value | collect
+        for z_field in z_type | out_rels[RT.GQL_Field]:
+            field_name = z_field | F.Name | collect
             Type.set_field(field_name,
                             P(resolve_field, z_field=z_field))
 
@@ -121,7 +125,7 @@ def generate_resolvers_fcts(schema_root):
                 assert op_is_unique(z_field), "Upfetch field must be unique"
                 assert is_required, "Upfetch field must be required"
 
-            base_field_type = z_field | target >> RT.Name | value | collect
+            base_field_type = z_field | target | F.Name | collect
             if is_scalar:
                 base_ref_field_type = base_field_type
             else:
@@ -254,11 +258,11 @@ def generate_resolvers_fcts(schema_root):
                 f"upfetch{name}(input: [Upfetch{name}Input!]!): Mutate{name}Response",
             ]
 
-    for z_enum in schema_root >> L[RT.GQL_Enum]:
-        name = value(z_enum >> RT.Name)
+    for z_enum in schema_root | Outs[RT.GQL_Enum]:
+        name = z_enum | F.Name | collect
 
         opts = {}
-        for z_opt in z_enum >> L[RT.GQL_Field]:
+        for z_opt in z_enum | Outs[RT.GQL_Field]:
             assert is_a(z_opt, AET.Enum(name))
             opt_en = value(z_opt)
             opts[opt_en.enum_value] = opt_en
@@ -274,7 +278,7 @@ def generate_resolvers_fcts(schema_root):
         all_objects += [Enum]
 
     # Always generate the Int scalar type 
-    int_type = schema_root >> L[RT.GQL_CoreScalarType] | filter[is_a[AET.Int]] | single | collect
+    int_type = schema_root | Outs[RT.GQL_CoreScalarType][AET.Int] | single | collect
     if int_type not in [rae_type(x) for x in extra_filters.keys()]:
         extra_filters[int_type] = schema_generate_scalar_filter(int_type)
 
@@ -309,7 +313,7 @@ def generate_resolvers_fcts(schema_root):
 #----------------------------------------------
 
 def schema_generate_list_params(z_type, extra_filters):
-    name = value(z_type >> RT.Name)
+    name = z_type | F.Name | collect
     if z_type not in extra_filters:
         extra_filters[z_type] = None
         extra_filters[z_type] = schema_generate_type_filter(z_type, extra_filters)
@@ -327,7 +331,7 @@ def schema_generate_list_params(z_type, extra_filters):
     return query_params
 
 def schema_generate_type_filter(z_type, extra_filters):
-    name = value(z_type >> RT.Name)
+    name = z_type | F.Name | collect
     fil_name = f"{name}Filter"
     list_fil_name = f"{name}FilterList"
     order_name = f"{name}Order"
@@ -347,10 +351,10 @@ def schema_generate_type_filter(z_type, extra_filters):
     # automatically generated "in"
     fields += [f"id: [ID!]"]
 
-    for field in z_type > L[RT.GQL_Field] | filter[op_is_searchable]:
-        field_name = value(field >> RT.Name)
+    for field in z_type | out_rels[RT.GQL_Field] | filter[op_is_searchable]:
+        field_name = field | F.Name | collect
         field_type = target(field)
-        field_type_name = value(field_type >> RT.Name)
+        field_type_name = field_type | F.Name | collect
         if is_a(field_type, AET.Bool):
             filter_name = Boolean
         else:
@@ -367,8 +371,8 @@ def schema_generate_type_filter(z_type, extra_filters):
         else:
             fields += [f"{field_name}: {filter_name}"]
 
-    for field in z_type > L[RT.GQL_Field] | filter[target | op_is_orderable]:
-        orderable_fields += [value(field >> RT.Name)]
+    for field in z_type | out_rels[RT.GQL_Field] | filter[target | op_is_orderable]:
+        orderable_fields += [field | F.Name | collect]
 
     fields = "\n\t".join(fields)
     orderable_fields = "\n\t".join(orderable_fields)
@@ -400,7 +404,7 @@ def schema_generate_type_filter(z_type, extra_filters):
         
 
 def schema_generate_scalar_filter(z_node):
-    type_name = z_node >> RT.Name | value | collect
+    type_name = z_node | F.Name | collect
     fil_name = f"{type_name}Filter"
     list_fil_name = f"{type_name}FilterList"
 
@@ -456,7 +460,7 @@ def resolve_get(_, info, *, type_node, **params):
 
 def resolve_query(_, info, *, type_node, **params):
     g = info.context["g"]
-    ents = g | now | all[ET(type_node >> RT.GQL_Delegate | collect)]
+    ents = g | now | all[ET(type_node | Out[RT.GQL_Delegate] | collect)]
     ents = ents | filter[pass_query_auth[type_node][info]]
 
     ents = handle_list_params(ents, type_node, params, info)
@@ -474,7 +478,7 @@ def resolve_aggregate(_, info, *, type_node, **params):
     for z_field in type_node > L[RT.GQL_Field] | filter[op_is_aggregable]:
         vals = ents | map[lambda z: resolve_field(z, info, z_field=z_field)] | filter[Not[equals[None]]] | collect
 
-        field_name = z_field >> RT.Name | value | collect
+        field_name = z_field | F.Name | collect
 
         if op_is_orderable(z_field | target):
             if len(vals) == 0:
@@ -528,47 +532,51 @@ def resolve_id(z, info):
 ##############################
 # * Mutations
 #----------------------------
+from threading import Lock
+mutation_lock = Lock()
+
 def resolve_add(_, info, *, type_node, **params):
     # TODO: @unique checks should probably be done post change as multiple adds
     # could try changing the same thing, including nested types.
     try:
-        name_gen = NameGen()
-        actions = []
-        post_checks = []
-        new_obj_names = []
-        updated_objs = []
-
-        # This is not optimal but simplest to understand for now.
-        upsert = params.get("upsert", False)
-        for item in params["input"]:
-            # Check id fields to see if we already have this.
-            if "id" in item:
-                if not upsert:
-                    raise ExternalError("Can't update item with id without setting upsert")
-                obj = find_existing_entity_by_id(info, type_node, item["id"])
-                if obj is None:
-                    raise Exception("Item doesn't exist")
-                set_d = {**item}
-                set_d.pop("id")
-                new_actions,new_post_checks = update_entity(obj, info, type_node, set_d, {}, name_gen)
-                actions += new_actions
-                post_checks += new_post_checks
-                updated_objs += [obj]
-            else:
-                obj_name,more_actions,more_post_checks = add_new_entity(info, type_node, item, name_gen)
-                actions += more_actions
-                post_checks += more_post_checks
-                new_obj_names += [obj_name]
+        with mutation_lock:
+            name_gen = NameGen()
+            actions = []
+            post_checks = []
+            new_obj_names = []
+            updated_objs = []
+
+            # This is not optimal but simplest to understand for now.
+            upsert = params.get("upsert", False)
+            for item in params["input"]:
+                # Check id fields to see if we already have this.
+                if "id" in item:
+                    if not upsert:
+                        raise ExternalError("Can't update item with id without setting upsert")
+                    obj = find_existing_entity_by_id(info, type_node, item["id"])
+                    if obj is None:
+                        raise Exception("Item doesn't exist")
+                    set_d = {**item}
+                    set_d.pop("id")
+                    new_actions,new_post_checks = update_entity(obj, info, type_node, set_d, {}, name_gen)
+                    actions += new_actions
+                    post_checks += new_post_checks
+                    updated_objs += [obj]
+                else:
+                    obj_name,more_actions,more_post_checks = add_new_entity(info, type_node, item, name_gen)
+                    actions += more_actions
+                    post_checks += more_post_checks
+                    new_obj_names += [obj_name]
 
-        r = commit_with_post_checks(actions, post_checks, info)
+            r = commit_with_post_checks(actions, post_checks, info)
 
-        ents = updated_objs + [r[name] for name in new_obj_names]
-        # Note: we return the details after any updates
-        ents = ents | map[now] | collect
+            ents = updated_objs + [r[name] for name in new_obj_names]
+            # Note: we return the details after any updates
+            ents = ents | map[now] | collect
 
-        count = len(new_obj_names)
+            count = len(new_obj_names)
 
-        return {"count": count, "ents": ents}
+            return {"count": count, "ents": ents}
     except ExternalError:
         raise
     except Exception as exc:
@@ -579,42 +587,43 @@ def resolve_upfetch(_, info, *, type_node, **params):
     # TODO: @unique checks should probably be done post change as multiple adds
     # could try changing the same thing, including nested types.
     try:
-        # Upfetch is a lot like add with upsert, except it uses the specially
-        # indicated upfetch field to work on, rather than id.
-        name_gen = NameGen()
-        actions = []
-        post_checks = []
-        new_obj_names = []
-        updated_objs = []
-
-        upfetch_field = op_upfetch_field(type_node)
-        field_name = upfetch_field >> RT.Name | value | collect
-
-        for item in params["input"]:
-            # Check the upfetch field to see if we already have this.
-            if field_name not in item:
-                raise Exception("Should never get here, because the upfetch field should be marked as required")
-
-            obj = find_existing_entity_by_field(info, type_node, upfetch_field, item[field_name])
-            if obj is None:
-                obj_name,more_actions,more_post_checks  = add_new_entity(info, type_node, item, name_gen)
-                actions += more_actions
-                post_checks += more_post_checks
-                new_obj_names += [obj_name]
-            else:
-                new_actions,new_post_checks = update_entity(obj, info, type_node, item, {}, name_gen)
-                actions += new_actions
-                post_checks += new_post_checks
-                updated_objs += [obj]
+        with mutation_lock:
+            # Upfetch is a lot like add with upsert, except it uses the specially
+            # indicated upfetch field to work on, rather than id.
+            name_gen = NameGen()
+            actions = []
+            post_checks = []
+            new_obj_names = []
+            updated_objs = []
+
+            upfetch_field = op_upfetch_field(type_node)
+            field_name = upfetch_field | F.Name | collect
+
+            for item in params["input"]:
+                # Check the upfetch field to see if we already have this.
+                if field_name not in item:
+                    raise Exception("Should never get here, because the upfetch field should be marked as required")
+
+                obj = find_existing_entity_by_field(info, type_node, upfetch_field, item[field_name])
+                if obj is None:
+                    obj_name,more_actions,more_post_checks  = add_new_entity(info, type_node, item, name_gen)
+                    actions += more_actions
+                    post_checks += more_post_checks
+                    new_obj_names += [obj_name]
+                else:
+                    new_actions,new_post_checks = update_entity(obj, info, type_node, item, {}, name_gen)
+                    actions += new_actions
+                    post_checks += new_post_checks
+                    updated_objs += [obj]
 
-        r = commit_with_post_checks(actions, post_checks, info)
+            r = commit_with_post_checks(actions, post_checks, info)
 
-        ents = updated_objs + [r[name] for name in new_obj_names]
-        # Note: we return the details after any updates
-        ents = ents | map[now] | collect
-        count = len(new_obj_names)
+            ents = updated_objs + [r[name] for name in new_obj_names]
+            # Note: we return the details after any updates
+            ents = ents | map[now] | collect
+            count = len(new_obj_names)
 
-        return {"count": count, "ents": ents}
+            return {"count": count, "ents": ents}
     except ExternalError:
         raise
     except Exception as exc:
@@ -626,27 +635,28 @@ def resolve_update(_, info, *, type_node, **params):
     # TODO: @unique checks should probably be done post change as multiple adds
     # could try changing the same thing, including nested types.
     try:
-        if "input" not in params or "filter" not in params["input"]:
-            raise Exception("Not allowed to update everything!")
-        ents = resolve_query(_, info, type_node=type_node, filter=params["input"]["filter"])
+        with mutation_lock:
+            if "input" not in params or "filter" not in params["input"]:
+                raise Exception("Not allowed to update everything!")
+            ents = resolve_query(_, info, type_node=type_node, filter=params["input"]["filter"])
 
-        actions = []
-        post_checks = []
+            actions = []
+            post_checks = []
 
-        name_gen = NameGen()
-        for ent in ents:
-            new_actions,new_post_checks = update_entity(ent, info, type_node, params["input"].get("set", {}), params["input"].get("remove", {}), name_gen)
-            actions += new_actions
-            post_checks += new_post_checks
+            name_gen = NameGen()
+            for ent in ents:
+                new_actions,new_post_checks = update_entity(ent, info, type_node, params["input"].get("set", {}), params["input"].get("remove", {}), name_gen)
+                actions += new_actions
+                post_checks += new_post_checks
 
-        commit_with_post_checks(actions, post_checks, info)
+            commit_with_post_checks(actions, post_checks, info)
 
-        count = len(ents)
+            count = len(ents)
 
-        # Note: we return the details after the update
-        ents = ents | map[now] | collect
+            # Note: we return the details after the update
+            ents = ents | map[now] | collect
 
-        return {"count": count, "ents": ents}
+            return {"count": count, "ents": ents}
     except ExternalError:
         raise
     except Exception as exc:
@@ -655,20 +665,25 @@ def resolve_update(_, info, *, type_node, **params):
 
 def resolve_delete(_, info, *, type_node, **params):
     try:
-        g = info.context["g"]
-        # Do the same thing as a resolve_query but delete the entities instead.
-        if "filter" not in params:
-            raise ExtenalError("Not allowed to delete everything!")
-        ents = resolve_query(_, info, type_node=type_node, **params)
+        with mutation_lock:
+            g = info.context["g"]
+            # Do the same thing as a resolve_query but delete the entities instead.
+            if "filter" not in params:
+                raise ExtenalError("Not allowed to delete everything!")
+            ents = resolve_query(_, info, type_node=type_node, **params)
 
-        if not ents | map[pass_delete_auth[type_node][info]] | all | collect:
-            raise ExtenalError("Auth check returned False")
+            if not ents | map[pass_delete_auth[type_node][info]] | all | collect:
+                raise ExtenalError("Auth check returned False")
 
-        [terminate[ent] for ent in ents] | transact[g] | run
+            post_checks = []
+            for ent in ents:
+                post_checks += [("remove", ent, type_node)]
+            actions = [terminate(ent) for ent in ents]
+            r = commit_with_post_checks(actions, post_checks, info)
 
-        count = len(ents)
+            count = len(ents)
 
-        return {"count": count, "ents": ents}
+            return {"count": count, "ents": ents}
     except ExternalError:
         raise
     except Exception as exc:
@@ -792,8 +807,8 @@ def scalar_comparison_op(sub):
     return this
                 
 def get_field_rel_by_name(z_type, name):
-    return (z_type > L[RT.GQL_Field]
-            | filter[Z >> RT.Name | value | equals[name]]
+    return (z_type | out_rels[RT.GQL_Field]
+            | filter[Z | F.Name | equals[name]]
             | first
             | collect)
 
@@ -847,25 +862,37 @@ def internal_resolve_field(z, info, z_field):
     
     is_incoming = z_field | op_is_incoming | collect
     # This is a delegate
-    relation = z_field >> RT.GQL_Resolve_With | collect
-    is_triple = source(relation) != relation
+    if z_field | has_out[RT.GQL_Resolve_With] | collect:
+        relation = z_field | Out[RT.GQL_Resolve_With] | collect
+        is_triple = source(relation) != relation
 
-    rt = RT(relation)
+        rt = RT(relation)
+
+        if is_triple:
+            if is_incoming:
+                assert rae_type(z) == rae_type(target(relation)), f"The RAET of the object {z} is not the same as that of the delegate relation {target(relation)}"
+            else:
+                assert rae_type(z) == rae_type(source(relation)), f"The RAET of the object {z} is not the same as that of the delegate relation {source(relation)}"
 
-    if is_triple:
         if is_incoming:
-            assert rae_type(z) == rae_type(target(relation)), f"The RAET of the object {z} is not the same as that of the delegate relation {target(relation)}"
+            opts = z | Ins[rt]
+            if is_triple:
+                opts = opts | filter[is_a[rae_type(source(relation))]]
         else:
-            assert rae_type(z) == rae_type(source(relation)), f"The RAET of the object {z} is not the same as that of the delegate relation {source(relation)}"
-
-    if is_incoming:
-        opts = z << L[rt]
-        if is_triple:
-            opts = opts | filter[is_a[rae_type(source(relation))]]
+            opts = z | Outs[rt]
+            if is_triple:
+                opts = opts | filter[is_a[rae_type(target(relation))]]
+    elif z_field | has_out[RT.GQL_FunctionResolver] | collect:
+        opts = func[z_field | Out[RT.GQL_FunctionResolver] | collect](z, info)
+        # This is to mimic the behaviour that people probably expect from a
+        # non-list resolver.
+        if type(opts) not in [list,tuple]:
+            if opts is None:
+                opts = []
+            else:
+                opts = [opts]
     else:
-        opts = z >> L[rt]
-        if is_triple:
-            opts = opts | filter[is_a[rae_type(target(relation))]]
+        raise Exception(f"Don't know how to resolve this field: {z_field}")
 
     # Auth filtering todo
     opts = opts | filter[pass_query_auth[target(z_field)][info]]
@@ -873,7 +900,11 @@ def internal_resolve_field(z, info, z_field):
     # We must convert final objects from AEs to python types.
     # if z_field | target | is_core_scalar | collect:
     if z_field | target | op_is_scalar | collect:
-        opts = opts | map[value]
+        # With a dynamic resolver, the items could also be values, so only apply value if they are ZefRefs
+        opts = opts | map[match[
+            (ZefRef, value),
+            (Any, identity)
+        ]]
 
     return opts
 
@@ -895,7 +926,7 @@ def find_existing_entity_by_id(info, type_node, id):
     
     g = info.context["g"]
 
-    et = ET(type_node >> RT.GQL_Delegate | collect)
+    et = ET(type_node | Out[RT.GQL_Delegate] | collect)
     ent = g[uid(id)] | now | collect
     if not is_a(ent, et):
         return None
@@ -909,7 +940,7 @@ def find_existing_entity_by_field(info, type_node, z_field, val):
         raise Exception("Can't find an entity by a None field value")
 
     g = info.context["g"]
-    et = ET(type_node >> RT.GQL_Delegate | collect)
+    et = ET(type_node | Out[RT.GQL_Delegate] | collect)
 
     # Note: we filter out with query auth even though this may mean we return
     # None instead of the actual entity. The follow-up logic with an upfetch
@@ -918,8 +949,8 @@ def find_existing_entity_by_field(info, type_node, z_field, val):
     # failure point at the same location so we can determine what kind of error
     # to return and whether this will leak sensitive information.
     ent = (g | now | all[et] | filter[pass_query_auth[type_node][info]]
-           | filter[internal_resolve_field[info][z_field] | single_or[None] | equals[val]]
-           | single_or[None]
+           | filter[internal_resolve_field[info][z_field] | optional | equals[val]]
+           | optional
            | collect)
 
     return ent
@@ -930,17 +961,17 @@ def add_new_entity(info, type_node, params, name_gen):
     post_checks = []
 
     this = str(next(name_gen))
-    type_name = type_node >> RT.Name | value | collect
+    type_name = type_node | F.Name | collect
 
     post_checks += [("add", this, type_node)]
 
-    et = ET(type_node >> RT.GQL_Delegate | collect)
+    et = ET(type_node | Out[RT.GQL_Delegate] | collect)
     actions += [et[this]]
 
     # This should probably be cached
     field_mapping = {}
-    for z_field in type_node > L[RT.GQL_Field]:
-        field_name = value(z_field >> RT.Name)
+    for z_field in type_node | out_rels[RT.GQL_Field]:
+        field_name = z_field | F.Name | collect
         field_mapping[field_name] = z_field
         # Convenient spot to validate that required fields have been specified.
         if op_is_required(z_field):
@@ -951,15 +982,15 @@ def add_new_entity(info, type_node, params, name_gen):
     for key,val in params.items():
         # TODO: Validate that any unique field is not duplicated
         z_field = field_mapping[key]
-        field_name = value(z_field >> RT.Name)
-        rt = RT(z_field >> RT.GQL_Resolve_With | collect)
+        field_name = z_field | F.Name | collect
+        rt = RT(z_field | Out[RT.GQL_Resolve_With] | collect)
         if z_field | op_is_list | collect:
             if not isinstance(val, list):
                 raise Exception(f"Value should have been list but was {type(val)}")
 
         if z_field | target | op_is_scalar | collect:
             if op_is_unique(z_field):
-                others = info.context["g"] | now | all[et] | filter[Z >> O[rt] | value_or[None] | equals[val]] | func[set] | collect
+                others = info.context["g"] | now | all[et] | filter[fvalue[rt][None] | equals[val]] | func[set] | collect
                 if len(others) > 0:
                     log.error("Trying to add a new entity with unique field that conflicts with others", et=et, field=field_name, others=others)
                     raise ExternalError(f"Unique field '{field_name}' conflicts with existing items.")
@@ -1003,7 +1034,7 @@ def find_or_add_entity(val, info, target_node, params, name_gen):
     
 
 def update_entity(z, info, type_node, set_d, remove_d, name_gen):
-    type_name = type_node >> RT.Name | value | collect
+    type_name = type_node | F.Name | collect
 
     # Refuse to set and remove the same thing. Just too confusing to deal with
     assert(len(set(set_d.keys()).intersection(remove_d.keys())) == 0), "Can't have the same set/remove keys"
@@ -1021,31 +1052,31 @@ def update_entity(z, info, type_node, set_d, remove_d, name_gen):
 
     # This should probably be cached
     field_mapping = {}
-    for z_field in type_node > L[RT.GQL_Field]:
-        field_mapping[value(z_field >> RT.Name)] = z_field
+    for z_field in type_node | out_rels[RT.GQL_Field]:
+        field_mapping[z_field | F.Name | collect] = z_field
 
     for key,val in set_d.items():
         # TODO: Validate that any unique field is not duplicated
         z_field = field_mapping[key]
-        field_name = value(z_field >> RT.Name)
+        field_name = z_field | F.Name | collect
         # TODO: This should be able to distinguish based on the triple, not just the RT
-        rt = RT(z_field >> RT.GQL_Resolve_With | collect)
+        rt = RT(z_field | Out[RT.GQL_Resolve_With] | collect)
 
         if op_is_list(z_field):
             raise Exception(f"Updating list things is a todo (for z_field={z_field})")
         else:
             if op_is_unique(z_field):
-                et = ET(type_node >> RT.GQL_Delegate | collect)
-                others = info.context["g"] | now | all[et] | filter[Z >> O[rt] | value_or[None] | equals[val]] | collect
+                et = ET(type_node | Out[RT.GQL_Delegate] | collect)
+                others = info.context["g"] | now | all[et] | filter[fvalue[rt][None] | equals[val]] | collect
                 others = set(others) - {z}
                 if len(others) > 0:
                     log.error("Trying to modify entity with unique field that conflicts with others", z=z, et=et, field=field_name, others=others)
                     raise ExternalError(f"Unique field '{field_name}' conflicts with existing items.")
 
             if op_is_incoming(z_field):
-                maybe_prior_rel = z < O[rt] | collect
+                maybe_prior_rel = z | in_rels[rt] | optional | collect
             else:
-                maybe_prior_rel = z > O[rt] | collect
+                maybe_prior_rel = z | out_rels[rt] | optional | collect
             if z_field | target | op_is_scalar | collect:
                 if maybe_prior_rel is None:
                     if op_is_incoming(z_field):
@@ -1053,7 +1084,7 @@ def update_entity(z, info, type_node, set_d, remove_d, name_gen):
                     else:
                         actions += [(z, rt, val)]
                 else:
-                    actions += [(maybe_prior_rel | target | collect) <= val]
+                    actions += [(maybe_prior_rel | target | assign[val] | collect)]
             else:
                 raise Exception("Updating non-scalars is TODO")
     
@@ -1061,20 +1092,20 @@ def update_entity(z, info, type_node, set_d, remove_d, name_gen):
         if val is not None:
             raise ExternalError("Remove vals need to be nil")
         z_field = field_mapping[key]
-        field_name = value(z_field >> RT.Name)
+        field_name = z_field | F.Name | collect
         if op_is_required(z_field):
             raise ExternalError(f"Not allowed to remove required field '{field_name}' on type '{type_name}'")
         # TODO: This should be able to distinguish based on the triple, not just the RT
-        rt = RT(z_field >> RT.GQL_Resolve_With | collect)
+        rt = RT(z_field | Out[RT.GQL_Resolve_With] | collect)
         if op_is_incoming(z_field):
-            rels = z < L[rt]
+            rels = z | in_rels[rt]
         else:
-            rels = z > L[rt]
-        actions += [terminate[rel] for rel in rels]
+            rels = z | out_rels[rt]
+        actions += [terminate(rel) for rel in rels]
 
         # Also delete scalars
         if z_field | target | op_is_scalar | collect:
-            actions += [terminate[target(rel)] for rel in rels]
+            actions += [terminate(target(rel)) for rel in rels]
 
     return actions, post_checks
 
@@ -1086,7 +1117,7 @@ def update_entity(z, info, type_node, set_d, remove_d, name_gen):
 def pass_auth_generic(z, schema_node, info, rt_list):
     to_call = None
     for rt in rt_list:
-        to_call = schema_node >> O[rt] | collect
+        to_call = schema_node | Outs[rt] | optional | collect
         if to_call is not None:
             break
     else:
@@ -1156,6 +1187,10 @@ def auth_helper_auth_field(field_name, auth, *, z, type_node, info):
         log.error("auth_field helper got an exception, assuming failure of auth")
         return False
 
+    if val is None:
+        # This is something we can't query on, so therefore the query has failed.
+        return False
+
     if auth == "query":
         func = pass_query_auth
     elif auth == "add":
@@ -1183,20 +1218,41 @@ def commit_with_post_checks(actions, post_checks, info):
                 if type(obj) == str:
                     obj = r[obj]
                 assert type(obj) == ZefRef
-                obj = now(obj)
-                type_name = {type_node >> O[RT.Name] | value_or[''] | collect}
+                obj = obj | in_frame[g | now | collect][allow_tombstone] | collect
+                type_name = {type_node | fvalue[RT.Name][''] | collect}
 
                 if kind == "add":
+                    for z_func in type_node | Outs[RT.OnCreate]:
+                        try:
+                            func[z_func](obj)
+                        except:
+                            raise ExternalError(f"OnCreate hook for type_node of '{type_name}' threw an exception")
                     if not pass_add_auth(obj, type_node, info):
-                        raise Exception(f"Add auth check for type_node of '{type_name}' returned False")
+                        raise ExternalError(f"Add auth check for type_node of '{type_name}' returned False")
                 elif kind == "update":
+                    for z_func in type_node | Outs[RT.OnUpdate]:
+                        try:
+                            func[z_func](obj)
+                        except:
+                            raise ExternalError(f"OnUpdate hook for type_node of '{type_name}' threw an exception")
                     if not pass_post_update_auth(obj, type_node, info):
-                        raise Exception(f"Post-update auth check for type_node of '{type_name}' returned False")
+                        raise ExternalError(f"Post-update auth check for type_node of '{type_name}' returned False")
+                elif kind == "remove":
+                    for z_func in type_node | Outs[RT.OnRemove]:
+                        try:
+                            func[z_func](obj)
+                        except:
+                            raise ExternalError(f"OnRemove hook for type_node of '{type_name}' threw an exception")
+
+
         except Exception as exc:
             log.error("Aborting transaction",
                       exc_info=exc)
             from ...pyzef.internals import AbortTransaction
             AbortTransaction(g)
-            raise ExternalError("Mutation did not pass auth checks")
+            if type(exc) == ExternalError:
+                raise exc
+            else:
+                raise ExternalError("Unexpected error in auth check/hooks execution")
 
     return r
diff --git a/python/zef/gql/simplegql/main.py b/python/zef/gql/simplegql/main.py
new file mode 100644
index 00000000..2dfac222
--- /dev/null
+++ b/python/zef/gql/simplegql/main.py
@@ -0,0 +1,78 @@
+# Copyright 2022 Synchronous Technologies Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+
+# These functions are here for __main__.py and whoever wants to write their own
+# main equivalent.
+
+
+from ... import *
+from ...ops import *
+
+from zef.core.logger import log
+
+from .schema_file_parser import parse_partial_graphql, json_to_minimal_nodes
+
+def create_schema_graph(schema_string, hooks_string=None):
+    g_schema = Graph()
+    if hooks_string is not None:
+        # Prep the schema graph with the hooks, so that the schema nodes can point directly at these
+        prepare_hooks(g_schema, hooks_string)
+
+    parsed_dict = parse_partial_graphql(schema_string)
+    commands = json_to_minimal_nodes(parsed_dict, g_schema)
+    r = commands | transact[g_schema] | run
+    root = r["root"]
+
+    return root
+
+
+def prepare_hooks(graph, hooks_string):
+    with Transaction(graph):
+        # Note that we save the string to a temporary file, because @func requires
+        # getsource which cannot identify the source from inside an arbitrary
+        # string.
+        import tempfile, os
+        fd,path = tempfile.mkstemp(prefix="SimpleGQL_hooks_", suffix=".py")
+        try:
+            with open(fd, "w") as file:
+                file.write(hooks_string)
+            code = compile(hooks_string, path, "exec")
+            globs = {"g": graph}
+            exec(code, globs, globs)
+        finally:
+            os.unlink(path)
+
+        # We could try and be fancy here with autodetection of names - but for the initial test let's just make the user do everything, including tagging nodes.
+
+        # Going to try autodetecting any zef functions on the graph
+        funcs = graph | now | all[ET.ZEF_Function] | collect
+
+        nodup_and_dup_funcs = (funcs
+                    | group_by[F.OriginalName]
+                    | group_by[second
+                                | length
+                                | greater_than_or_equal[2]]
+                            [[False,True]]
+                    | func[dict]
+                    | collect)
+
+        nodups = nodup_and_dup_funcs[False] | map[apply[first, second | single]] | collect
+        dups = nodup_and_dup_funcs[True] | map[first] | collect
+
+        if len(dups) > 0:
+            log.error("There are zef functions that have duplicate names on the schema graph. I can't autotag these functions and will ignore them.", dups=dups)
+
+        for name,z in nodups:
+            z | tag[name] | graph | run
\ No newline at end of file
diff --git a/python/zef/gql/simplegql/schema_file_parser.py b/python/zef/gql/simplegql/schema_file_parser.py
index d73d114c..421e1d52 100644
--- a/python/zef/gql/simplegql/schema_file_parser.py
+++ b/python/zef/gql/simplegql/schema_file_parser.py
@@ -17,6 +17,8 @@
 
 import json
 import graphql
+
+
 def parse_partial_graphql(schema):
     """Goes through the schema line by line and creates a json file that represents
     the schema one-to-one. This should be an invertible process.
@@ -29,10 +31,9 @@ def parse_partial_graphql(schema):
         "enums": {}
     }
 
-    
     # Find all Zef directives first, which are single-line comments that begin with # Zef.<name>:
-    lines = (schema 
-             | split["\n"] 
+    lines = (schema
+             | split["\n"]
              | filter[lambda x: x.startswith("# Zef.")]
              | map[lambda x: x[len("# Zef."):]]
              | collect)
@@ -40,7 +41,7 @@ def parse_partial_graphql(schema):
     for line in lines:
         parts = line.split(':', 1)
         assert len(parts) == 2, "Zef directive line contains no ':'."
-        name,details = parts
+        name, details = parts
         name = name.strip()
 
         if name == "SchemaVersion":
@@ -48,7 +49,6 @@ def parse_partial_graphql(schema):
             output["schema_version"] = details.strip()
             continue
 
-
         # Anything else requires json parsing
         try:
             details = json.loads(details)
@@ -58,14 +58,15 @@ def parse_partial_graphql(schema):
         if name == "Authentication":
             assert "auth" not in output, "Not allowed to have multiple Zef.Authentication directives."
             for key in details:
-                assert key in ["Algo", "JWKURL", "Audience", "Header", "Namespace", "VerificationKey", "VerificationKeyEnv", "Public"], f"Unknown auth key '{key}'"
+                assert key in ["Algo", "JWKURL", "Audience", "Header", "Namespace",
+                               "VerificationKey", "VerificationKeyEnv", "Public"], f"Unknown auth key '{key}'"
             output["auth"] = details
         else:
             raise Exception(f"Unsupported Zef.{name} directive")
 
-
     if output.get("schema_version", None) != "v1":
-        raise Exception("Only support schemas with an explicit version of 'v1'. Please include:\n# Zef.SchemaVersion: v1\ninto your schema file.")
+        raise Exception(
+            "Only support schemas with an explicit version of 'v1'. Please include:\n# Zef.SchemaVersion: v1\ninto your schema file.")
 
     for definition in doc.definitions:
         if definition.kind == "object_type_definition":
@@ -74,7 +75,8 @@ def parse_partial_graphql(schema):
             for directive in definition.directives:
                 if directive.name.value == "auth":
                     if "auth" not in output:
-                        raise Exception("Disallowing @auth directives when no # Zef.Authentication... line is present to prevent accidentally security holes.")
+                        raise Exception(
+                            "Disallowing @auth directives when no # Zef.Authentication... line is present to prevent accidentally security holes.")
                     for arg in directive.arguments:
                         if arg.name.value == "query":
                             t_def["_AllowQuery"] = arg.value.value
@@ -100,6 +102,14 @@ def parse_partial_graphql(schema):
                     if arg.name.value != "et":
                         raise Exception("ET directive needs exactly one argument, 'et'")
                     t_def["_ET"] = arg.value.value
+                elif directive.name.value == "hook":
+                    for arg in directive.arguments:
+                        for hook_name in ["Create", "Remove", "Update"]:
+                            if arg.name.value == f"on{hook_name}":
+                                t_def[f"_On{hook_name}"] = arg.value.value
+                                break
+                        else:
+                            raise Exception(f"Unknown hook name {arg.name.value}")
                 else:
                     raise Exception(f"Don't know how to handle type directive @{directive.name.value}")
 
@@ -135,8 +145,10 @@ def parse_partial_graphql(schema):
                                 f_def["relation"] = (ET(args["source"]), RT(args["rt"]), ET(args["target"]))
                             else:
                                 f_def["relation"] = RT(args["rt"])
-                        elif v == "resolver":
-                            raise Exception("@resolver not yet implemented")
+                        elif v == "dynamic":
+                            args = parse_arguments_as_dict(directive.arguments)
+                            assert args.keys() == {"hook"}, "@dynamic must take a single argument, 'hook'"
+                            f_def["dynamic"] = args["hook"]
                         else:
                             raise Exception(f"Don't know how to handle directive @{v}")
         elif definition.kind == "enum_type_definition":
@@ -150,10 +162,9 @@ def parse_partial_graphql(schema):
         else:
             raise Exception(f"Don't know how to handle a top-level graphql schema object of kind {definition.kind}")
 
-
-
     return output
 
+
 def string_to_RAET(s):
     if s.startswith("ET."):
         return ET(s[len("ET."):])
@@ -166,21 +177,22 @@ def string_to_RAET(s):
     else:
         raise Exception(f"Can't parse string as a RAE type: {s}")
 
+
 def parse_arguments_as_dict(args):
     d = {}
     for arg in args:
         d[arg.name.value] = arg.value.value
     return d
-    
-        
+
+
 def simple_capitalize(s):
     # This is here to make things like firebaseID go to FirebaseID rather than
     # FirebaseId. This is more predicatable for users.
     assert len(s) > 0
     return s[0].upper() + s[1:]
-    
 
-def json_to_minimal_nodes(json):
+
+def json_to_minimal_nodes(json, g):
     """This takes a json schema, such as one produced by the `parse_schema`
         function, and puts the minimal GQL schema nodes onto the graph.
         "Minimal" is the key word here, as the generated schema graph nodes will
@@ -247,7 +259,7 @@ def name_to_raet(name):
         if name in json["enums"]:
             return getattr(AET.Enum, name)
         return ET(name)
-    
+
     for type_name,fields in json["types"].items():
 
         actions += [(ET.GQL_Type[type_name], RT.Name, type_name)]
@@ -265,12 +277,17 @@ def name_to_raet(name):
                     continue
                 elif field_name in ["_AllowQuery", "_AllowAdd", "_AllowUpdate", "_AllowUpdatePost", "_AllowDelete"]:
                     # TODO: Turn into a zef function later on
-                    actions += [(Z[type_name], RT(field_name[1:]), field),]
+                    actions += [(Z[type_name], RT(field_name[1:]), field)]
+                elif field_name in ["_OnCreate", "_OnRemove", "_OnUpdaate"]:
+                    # Find the zef function that this corresponds to
+                    z_func = g[field]
+                    actions += [(Z[type_name], RT(field_name[1:]), z_func)]
+
                 elif field_name == "_Upfetch":
                     assert field in fields and "unique" in fields[field] and "required" in fields[field], f"Upfetch field '{field}' must exist, be unique and be required."
                     # We can figure out which relation we can attach the upfetch bool to.
                     qual_name = type_name + "__" + field
-                    actions += [(Z[qual_name], RT.Upfetch, True),]
+                    actions += [(Z[qual_name], RT.Upfetch, True)]
                 else:
                     raise Exception(f"Don't understand special field name '{field_name}'")
             else:
@@ -286,9 +303,11 @@ def name_to_raet(name):
                         actions += [(Z[qual_name], RT(simple_capitalize(bool_key)), field[bool_key])]
                         del field[bool_key]
 
-                if "resolver" in field:
-                    raise NotImplementedError("TODO")
-                    del field["resolver"]
+                if "dynamic" in field:
+                    assert "relation" not in field, f"Can't give a relation with a dynamically resolved field: {field}"
+                    z_func = g[field["dynamic"]]
+                    actions += [(Z[qual_name], RT.GQL_FunctionResolver, z_func)]
+                    del field["dynamic"]
                 else:
                     if "relation" in field:
                         resolve_with = delegate_of(field["relation"])
@@ -315,3 +334,6 @@ def name_to_raet(name):
             actions += [(Z[enum_name], RT.GQL_Field, EN(enum_name, opt))]
 
     return actions
+
+
+
diff --git a/python/zef/gql/simplegql/server.py b/python/zef/gql/simplegql/server.py
index e6bf4548..529de753 100644
--- a/python/zef/gql/simplegql/server.py
+++ b/python/zef/gql/simplegql/server.py
@@ -28,10 +28,10 @@
 def resolve_token(context, request):
     root = context["z_gql_root"]
 
-    header = root >> RT.AuthHeader | value | collect
-    aud = root >> RT.AuthAudience | value | collect
-    namespace = root >> O[RT.AuthNamespace] | value_or[None] | collect
-    algo = root >> RT.AuthAlgo | value | collect
+    header = root | F.AuthHeader | collect
+    aud = root | F.AuthAudience | collect
+    namespace = root | Outs[RT.AuthNamespace] | single_or[None] | value_or[None] | collect
+    algo = root | F.AuthAlgo | collect
 
     import jwt
     import base64
@@ -56,7 +56,7 @@ def resolve_token(context, request):
                                  algorithms=[algo],
                                  audience=aud)
     elif algo == "HS256":
-        psk = root >> RT.AuthPresharedKey | value | collect
+        psk = root | F.AuthPresharedKey | collect
         auth_result = jwt.decode(token, psk, algorithms=[algo],
                                  audience=aud)
     else:
@@ -75,7 +75,7 @@ def query(request, context):
     if root | has_out[RT.AuthHeader] | collect:
         try:
             auth_context = resolve_token(context, request)
-            if auth_context is None and not (root >> O[RT.AuthPublic] | value_or[True] | collect):
+            if auth_context is None and not (root | Outs[RT.AuthPublic] | single_or[None] | value_or[True] | collect):
                 raise Exception("No auth and public is False.")
         except Exception as exc:
             log.error("Auth failed", exc_info=exc)
@@ -114,13 +114,14 @@ def start_server(z_gql_root,
                  g_data,
                  port=443,
                  bind_address="0.0.0.0",
+                 logging=True,
                  ):
 
     schema,objects = generate_resolvers_fcts(z_gql_root)
     ari_schema = make_executable_schema(schema, objects)
 
-    import logging
-    logger = logging.getLogger("ariadne").disabled = True
+    from logging import getLogger
+    logger = getLogger("ariadne").disabled = True
     log.debug("Disabled ariadne logging")
     
     context = {
@@ -130,7 +131,7 @@ def start_server(z_gql_root,
     }
 
     if z_gql_root | has_out[RT.AuthJWKURL] | collect:
-        url = z_gql_root >> RT.AuthJWKURL | value | collect
+        url = z_gql_root | F.AuthJWKURL | collect
 
         from jwt import PyJWKClient
         context["jwk_client"] = PyJWKClient(url)
@@ -144,7 +145,7 @@ def start_server(z_gql_root,
                                            fallback_not_found,
                                             send_response]]
                       | subscribe[run]),
-        'logging': True,
+        'logging': logging,
         'bind_address': bind_address,
     }) | run
     if is_a(http_r, Error):
diff --git a/python/zef/ops/__init__.py b/python/zef/ops/__init__.py
index 20868460..f5b1284a 100644
--- a/python/zef/ops/__init__.py
+++ b/python/zef/ops/__init__.py
@@ -33,4 +33,5 @@
     "_any",
     "value_or",
     "maybe_value",
+    "config",
 ] + privileges.__all__ + lazy_all + transactor_role.__all__
\ No newline at end of file
diff --git a/python/zef/ops/_config.py b/python/zef/ops/_config.py
index 12fe477d..6a30d89a 100644
--- a/python/zef/ops/_config.py
+++ b/python/zef/ops/_config.py
@@ -18,131 +18,25 @@
 ]
 
 import os
-from pathlib import Path
 import yaml
 from ..core.op_implementations.dispatch_dictionary import _op_to_functions
 from ..core import *
 from ..core._ops import *
+from ..pyzef import main as pymain
 
-from dataclasses import dataclass
-import typing
-@dataclass
-class ConfigItem:
-    path: str
-    # These two types will have to be handled later on
-    typ: typing.Any
-    default: typing.Any
-    env_var: str
-    doc: str
-    
-
-config_spec = [
-    # Options are "false", "auto", "always"
-    ConfigItem("login.autoConnect", None, "auto", "ZEFDB_LOGIN_AUTOCONNECT", "Whether zefdb should automatically connect to ZefHub on start of the butler."),
-    # In the future, this should be true not "true"
-    ConfigItem("butler.autoStart", bool, "true", "ZEFDB_BUTLER_AUTOSTART", "Whether the butler will automatically be started on import of the zefdb module."),
-]
-config_spec = {x.path: x for x in config_spec}
-# Anything that has been manually set in this session will override any environment variable etc...
-session_overrides = {}
-
-
-config_file_path = Path.home() / ".zef" / "config.yaml"
-
-
-def config_file_exists():
-    return os.path.exists(config_file_path)
-
-def ensure_config_file():
-    if os.path.exists(config_file_path):
-        return
-    config_file_path.parent.mkdir(parents=True, exist_ok=True)
-    # if not os.access(str(config_file_path), os.W_OK):
-    #     raise Exception(f"Don't have permissions to write to config file path: {config_file_path}") 
-    with open(config_file_path, 'w'):
-        pass
-
-def nested_set(dic, keys, value):
-    for key in keys[:-1]:
-        dic = dic.setdefault(key, {})
-    dic[keys[-1]] = value
-
-
-def get_config(key=""):
-    # TODO: Danny did this quickly, but it would only work for specific keys.
-    # Need to handle wildcards/group selections.
-    if key in session_overrides:
-        return session_overrides[key]
-    if key in config_spec:
-        spec = config_spec[key]
-        import os
-        if spec.env_var in os.environ:
-            # TODO: Handle conversion from string here
-            return os.environ[spec.env_var]
-
-    # Check if config file exists
-    if config_file_exists():
-        with open(config_file_path, 'r') as stream:
-            try:
-                config = yaml.safe_load(stream)
-                if config is None:
-                    # TODO: Handle warnings about improper config here
-                    return None
-
-                # Check if key is empty or wildcard
-                if key == "" or key == "*":
-                    return config
-
-                # Check if key is in config and return value
-                for k in key.split("."):
-                    if k not in config:
-                        config = None
-                        break
-                    config = config[k]
-                if config is not None:
-                    return config
-            except yaml.YAMLError as exc:
-                print(exc)
-
-    # This is the default option - we also assume that by getting here we must
-    # have a valid config key, so error if nothing matches.
-    if key not in config_spec:
-        raise Exception(f"Unknown config option {key}")
-
-    return config_spec[key].default
-        
-    
-
+def get_config(key):
+    return pymain.get_config_var(key)
 
 def set_config(key, val):
-    ensure_config_file()
-    try:
-        with open(config_file_path, 'r') as stream:
-            config = yaml.safe_load(stream)
-            if key == "":
-                return config
-            if config is None:
-                config = {}
-
-            nested_set(config, key.split("."), val)
-            yamlConfig = yaml.safe_dump(config, default_flow_style=False)
-        with open(config_file_path, 'w') as filehandle:
-            filehandle.write(yamlConfig)
-    except yaml.YAMLError as exc:
-        print(exc)
+    return pymain.set_config_var(key, val)
 
-    if key in config_spec:
-        session_overrides[key] = val
+def list_config(filter):
+    tuples = pymain.list_config(filter)
 
-
-def list_config():
-    if config_file_exists():
-        with open(config_file_path, 'r') as stream:
-            try:
-                config = yaml.safe_load(stream)
-                # TODO: Print ascii table?
-            except yaml.YAMLError as exc:
-                print(exc)
+    d = {}
+    for path,val in tuples:
+        d = insert_in(d, path.split('.'), val)
+    return d
 
 
 def config_implementation(payload, action):
@@ -154,6 +48,9 @@ def config_implementation(payload, action):
     elif action == KW.get:
         assert isinstance(payload, str)
         return get_config(payload)
+    elif action == KW.list:
+        assert isinstance(payload, str)
+        return list_config(payload)
     else:
         raise NotImplementedError("Action {} not implemented".format(action))
 
diff --git a/python/zef/ops/privileges.py b/python/zef/ops/privileges.py
index 21babadd..1c41943a 100644
--- a/python/zef/ops/privileges.py
+++ b/python/zef/ops/privileges.py
@@ -18,6 +18,7 @@
     "login",
     "login_as_guest",
     "logout",
+    "who_am_i",
 ]
 
 from ..core import *
@@ -65,7 +66,5 @@ def privileges_type_info(op, curr_type):
 login = Effect({"type": FX.ZefHub.Login})
 login_as_guest = Effect({"type": FX.ZefHub.Login, "auth_key": "GUEST"})
 logout = Effect({"type": FX.ZefHub.Logout})
-# We don't want complaints when these are garbage collected
-login._been_run = True
-login_as_guest._been_run = True
-logout._been_run = True
+
+from ..pyzef.internals import who_am_i
\ No newline at end of file


Test patch:
diff --git a/python/tests/requirements_tests.txt b/python/tests/requirements_tests.txt
index bebde7df..f33a9d62 100644
--- a/python/tests/requirements_tests.txt
+++ b/python/tests/requirements_tests.txt
@@ -1,3 +1,4 @@
 pytest
 networkx>=2.6.3
 requests
+pyjwt>=2.2.0
diff --git a/python/tests/test_attach.py b/python/tests/test_attach.py
index 4c7ffa31..aa5a6ef3 100644
--- a/python/tests/test_attach.py
+++ b/python/tests/test_attach.py
@@ -38,6 +38,40 @@ def test_attach(self):
         self.assertEqual(length(z2 | now | out_rels[RT]), 2)
         self.assertEqual(length(z3 | now | out_rels[RT]), 5)
 
+    def test_set_field(self):
+        g = Graph()
+
+        z = ET.Machine | g | run
+        z | set_field[RT.Weight][QuantityFloat(100.0, EN.Unit.kilogram)] | g | run
+        z2 = ET.Machine | g | run
+        z2 | set_field[RT.Name]["asdf"] | g | run
+        z2 | set_field[RT.Status]["asdf"] | g | run
+        z3 = ET.Machine | g | run
+        [z3 | set_field[RT.Number][5] | collect,
+         z3 | set_field[RT.Time][now()] | collect,
+         z3 | set_field[RT.Fraction][0.5] | collect,
+         z3 | set_field[RT.Status][EN.Status.On] | collect,
+         z3 | set_field[RT.Disabled][True]
+         ] | transact[g] | run
+
+        self.assertEqual(length(z | now | out_rels[RT]), 1)
+        self.assertEqual(length(z2 | now | out_rels[RT]), 2)
+        self.assertEqual(length(z3 | now | out_rels[RT]), 5)
+
+        r = [ET.Machine["z"],
+             ET.Person["y"],
+             Z["z"] | set_field[RT.Supervisor][Z["y"]],
+             ] | transact[g] | run
+        self.assertEqual(r["z"] | Out[RT.Supervisor] | collect, r["y"])
+        
+        r2 = [ET.Machine["z"] | set_field[RT.Supervisor][ET.Person["y"]]] | transact[g] | run
+        self.assertEqual(r2["z"] | Out[RT.Supervisor] | collect, r2["y"])
+
+        r3 = [r["z"] | set_field[RT.Supervisor][ET.Person["new"]]] | transact[g] | run
+        self.assertEqual(r["z"] | now | Outs[RT.Supervisor] | length | collect, 1)
+        self.assertEqual(r["z"] | now | Out[RT.Supervisor] | collect, r3["new"])
+
+
 
     def test_shorthand(self):
         g = Graph()
@@ -81,10 +115,10 @@ def test_shorthand_assign(self):
         ae = AET.String | g | run
         self.assertEqual(value(ae), None)
 
-        ae | assign_value["word"] | g | run
+        ae | assign["word"] | g | run
         self.assertEqual(value(now(ae)), "word")
 
-        g | now | all[AET] | single | assign_value["single"] | g | run
+        g | now | all[AET] | single | assign["single"] | g | run
         self.assertEqual(value(now(ae)), "single")
 
 if __name__ == '__main__':
diff --git a/python/tests/test_graph_delta.py b/python/tests/test_graph_delta.py
index 99234b49..e5e901a0 100644
--- a/python/tests/test_graph_delta.py
+++ b/python/tests/test_graph_delta.py
@@ -47,7 +47,7 @@ def test_basics(self):
         z_joe = r["joe"]
         r2 = [
             (r["joe"], RT.NickName, "Jay"),
-            r["joe"] | Out[RT.LastName] | assign_value["Smith"],
+            r["joe"] | Out[RT.LastName] | assign["Smith"],
         ] | transact[g] | run
 
         z2_joe = now(z_joe)
@@ -155,5 +155,18 @@ def test_tagging(self):
         self.assertEqual(r["joe"], g | now | get["secret"] | collect)
         self.assertEqual(z | now | collect, g | now | get["first"] | collect)
 
+    def test_dictionary(self):
+        g = Graph()
+
+        y = ET.Person | g | run
+        z = {ET.Person: {
+            RT.Name: "Joe",
+            RT.Supervisor: y
+        }} | g | run
+
+        self.assertEqual(z | Out[RT.Supervisor] | origin_rae | collect,
+                         y | origin_rae | collect)
+        self.assertEqual(z | F.Name | collect, "Joe")
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/python/tests/rem_test_http.py b/python/tests/test_http.py
similarity index 96%
rename from python/tests/rem_test_http.py
rename to python/tests/test_http.py
index f3f39c76..0777f22f 100644
--- a/python/tests/rem_test_http.py
+++ b/python/tests/test_http.py
@@ -21,7 +21,7 @@ def test_http_server(self):
         from zef.core.fx.http import send_response, permit_cors, middleware, middleware_worker, fallback_not_found, route
         port = 4990
         payload = b"successful test"
-        eff = Effect({
+        eff = {
             'type': FX.HTTP.StartServer,
             # We hope that this is not taken by anything else for this test...
             'port': port,
@@ -31,7 +31,7 @@ def test_http_server(self):
                                                 send_response]]
                           | subscribe[run]),
             'bind_address': "localhost",
-        })
+        }
 
         eff_resp = eff | run
 
@@ -43,10 +43,10 @@ def test_http_server(self):
         r = requests.get(f"http://localhost:{port}")
         self.assertEqual(r.status_code, 404)
 
-        Effect({
+        {
             "type": FX.HTTP.StopServer,
             "server_uuid": eff_resp["server_uuid"],
-        }) | run
+        } | run
 
 if __name__ == '__main__':
     unittest.main()
diff --git a/python/tests/test_nx_wrapper.py b/python/tests/test_nx_wrapper.py
index bdc69428..821a85c4 100644
--- a/python/tests/test_nx_wrapper.py
+++ b/python/tests/test_nx_wrapper.py
@@ -50,9 +50,9 @@ def test_networkx(self):
 
 
         dg = ProxyGraph(now(g), ET.Person, RT.Knows)
-        self.assertEqual(set(dg.nodes) | map[lambda x: x.z] | func[set] | collect,
+        self.assertEqual(set(dg.nodes) | func[set] | collect,
                          set(g | now | all[ET.Person] | collect))
-        self.assertEqual(set(dg.edges) | map[map[lambda x: x.z] | func[tuple]] | func[set] | collect,
+        self.assertEqual(set(dg.edges) | func[set] | collect,
                          set([(r["alex"], r["bob"]),
                               (r["alex"], r["zach"])]))
         self.assertEqual(dg[r["alex"]][r["bob"]]["From"], "University")
@@ -74,14 +74,14 @@ def test_networkx(self):
         nx.node_connectivity(ug, list(dg.nodes)[1], list(dg.nodes)[2])
         nx.node_connectivity(dg, list(dg.nodes)[1], list(dg.nodes)[2])
 
-        last_two = [r["bob"], r["zach"]]
+        last_two = {r["bob"], r["zach"]}
         dg_sg = dg.subgraph(last_two)
         ug_sg = ug.subgraph(last_two)
         self.assertEqual(nx.node_connectivity(ug), 1)
         self.assertEqual(nx.node_connectivity(ug_sg), 0)
 
         nx.k_components(ug)
-        nx.k_components(dg2.to_undirected())
+        nx.k_components(dg2.to_undirected().to_native())
 
         # Can't work - tries to construct a graph itself
         # nx.maximum_branching(dg)
diff --git a/python/tests/test_oldstyle.py b/python/tests/test_oldstyle.py
index 2a58c0b3..c3e4ed94 100644
--- a/python/tests/test_oldstyle.py
+++ b/python/tests/test_oldstyle.py
@@ -81,17 +81,17 @@ def test_find_source_tx(self):
         zs = g | now | all[ET.Person] | collect
         self.assertEqual(len(zs), 5)
 
-    def test_assign_value(self):
+    def test_assign(self):
         g = Graph()
         with Transaction(g):
             name = instantiate(AET.String, g)
             age =  instantiate(AET.Int, g)
 
-        assign_value(name, 'Lu\0na\nis\nhungry') | g | run
-        assign_value(age, 42) | g | run
+        assign(name, 'Lu\0na\nis\nhungry') | g | run
+        assign(age, 42) | g | run
         num = instantiate(AET.Int, g)
-        assign_value(name, 'Naomi') | g | run
-        assign_value(age, 43) | g | run
+        assign(name, 'Naomi') | g | run
+        assign(age, 43) | g | run
 
         name | terminate | g | run
 
diff --git a/python/tests/test_partial_graph.py b/python/tests/test_partial_graph.py
index 6b490dd5..9820e20c 100644
--- a/python/tests/test_partial_graph.py
+++ b/python/tests/test_partial_graph.py
@@ -18,7 +18,7 @@
 import zef
 
 class MyTestCase(unittest.TestCase):
-    def test_zefrefs_from_slices(self):
+    def test_partial_graphs_same(self):
         g = Graph()
 
         g_clones = []
@@ -35,13 +35,70 @@ def test_zefrefs_from_slices(self):
         z | terminate | g | run
         g_clones += [g]
 
-        for i,tx in enumerate(g | all[TX] | drop[1] | collect):
+        for i,tx in enumerate(g | all[TX] | skip[1] | collect):
             g_clone_before_tx = g_clones[i]
 
             for j in range(i+1,len(g_clones)):
                 g_partial = zef.pyzef.internals.create_partial_graph(g_clones[j], g_clone_before_tx.graph_data.write_head)
                 self.assertEqual(g_clone_before_tx.hash(), g_partial.hash())
 
+    def test_abort_transaction(self):
+        g = Graph()
+
+        z = ET.Machine | g | run
+
+        before_hash = g.hash()
+
+        with Transaction(g):
+            z2 = ET.Machine | g | run
+            zef.pyzef.internals.AbortTransaction(g)
+        self.assertEqual(before_hash, g.hash())
+
+        with Transaction(g):
+            (z, RT.Something, 5) | g | run
+            zef.pyzef.internals.AbortTransaction(g)
+        self.assertEqual(before_hash, g.hash())
+
+        with Transaction(g):
+            z | terminate | g | run
+            zef.pyzef.internals.AbortTransaction(g)
+        self.assertEqual(before_hash, g.hash())
+
+        dz = delegate_of(z)
+        (dz, RT.Meta, 1) | g | run
+        before_hash = g.hash()
+
+        with Transaction(g):
+            (dz, RT.Something, 5) | g | run
+            zef.pyzef.internals.AbortTransaction(g)
+        self.assertEqual(before_hash, g.hash())
+
+        z,_,ae = (z, RT.Something, 1) | g | run
+        before_hash = g.hash()
+
+        with Transaction(g):
+            ae | assign[2] | g | run
+            zef.pyzef.internals.AbortTransaction(g)
+        self.assertEqual(before_hash, g.hash())
+
+    def test_empty_transaction(self):
+        g = Graph()
+
+        num_txs = len(g | all[TX] | collect)
+        before_read_head = g.graph_data.read_head
+
+        with Transaction(g, True, True):
+            pass
+
+        self.assertEqual(num_txs, len(g | all[TX] | collect))
+        self.assertEqual(before_read_head, g.graph_data.read_head)
+
+        with Transaction(g, True, False):
+            pass
+
+        self.assertEqual(True, num_txs < len(g | all[TX] | collect))
+        self.assertEqual(True, before_read_head < g.graph_data.read_head)
+
         
 
 if __name__ == '__main__':
diff --git a/python/tests/test_schema.py b/python/tests/test_schema.py
index d50579a9..2fd99e50 100644
--- a/python/tests/test_schema.py
+++ b/python/tests/test_schema.py
@@ -25,14 +25,14 @@ def test_basics(self):
 
         z = ET.Machine | g | run
 
-        self.assertEqual(g | schema | length | collect, 2)
+        self.assertEqual(g | blueprint | length | collect, 2)
 
         a,b,c = (z, RT.Something, 5) | g | run
 
-        self.assertEqual(g | schema | length | collect, 5)
+        self.assertEqual(g | blueprint | length | collect, 4)
 
-        self.assertEqual(g | now | schema | length | collect, 5)
-        self.assertEqual(z | frame | schema | length | collect, 2)
+        self.assertEqual(g | now | blueprint | length | collect, 4)
+        self.assertEqual(z | frame | blueprint | length | collect, 2)
 
         dz = delegate_of(z)
         db = delegate_of(b)
@@ -47,10 +47,10 @@ def test_basics(self):
 
         zef.pyzef.zefops.retire(to_ezefref(db))
 
-        self.assertEqual(g | now | schema | length | collect, 4)
+        self.assertEqual(g | now | blueprint | length | collect, 3)
 
         zef.pyzef.zefops.retire(to_ezefref(dz))
-        self.assertEqual(g | now | schema | length | collect, 3)
+        self.assertEqual(g | now | blueprint | length | collect, 2)
 
     def test_meta(self):
         g = Graph()
@@ -91,16 +91,16 @@ def test_recreate_delegate(self):
 
         z = ET.Machine | g | run
 
-        self.assertEqual(g | now | schema | length | collect, 2)
+        self.assertEqual(g | now | blueprint | length | collect, 2)
 
         z | terminate | g | run
         zef.pyzef.zefops.retire(to_ezefref(delegate_of(z)))
 
-        self.assertEqual(g | now | schema | length | collect, 1)
+        self.assertEqual(g | now | blueprint | length | collect, 1)
 
         z = ET.Machine | g | run
 
-        self.assertEqual(g | now | schema | length | collect, 2)
+        self.assertEqual(g | now | blueprint | length | collect, 2)
 
 
 
diff --git a/python/tests/test_simplegql.py b/python/tests/test_simplegql.py
new file mode 100644
index 00000000..7bfb087a
--- /dev/null
+++ b/python/tests/test_simplegql.py
@@ -0,0 +1,208 @@
+# Copyright 2022 Synchronous Technologies Pte Ltd
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#     http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import unittest  # pytest takes ages to run anything as soon as anything from zef is imported
+from zef import *
+from zef.ops import *
+
+class MyTestCase(unittest.TestCase):
+    def setUp(self):
+        self.key = "asdf"
+        schema_gql = """
+# Zef.SchemaVersion: v1
+# Zef.Authentication: {"Algo": "HS256", "VerificationKey": \"""" + self.key + """\", "Audience": "test", "Header": "X-Auth-Token"}
+        
+type User
+  @auth(
+    add: "info.context | get_in[('auth', 'admin')][False]"
+    query: "F.Email | equals[info.context | get_in[('auth', 'email')][None] | collect]"
+  )
+  @upfetch(field: "email")
+  @hook(onCreate: "userCreate")
+ {
+  email: String! @unique @search
+  transactions: [Transaction]
+    @incoming
+    @relation(source: "Transaction", rt: "User", target: "User")
+    @search
+
+  testString: String
+  testEnum: TestEnum! @search
+  testOut: Transaction
+  testTime: DateTime
+  testDynamic: String @dynamic(hook: "dynamicHook") @search
+}
+
+type Transaction
+  @auth(
+    query: "auth_field('user', 'query')"
+  ) {
+  user: User @relation(rt: "User") @search
+  amount: Int @search
+  date: DateTime @search
+}
+
+enum TestEnum {
+    ABC
+    DEF
+}
+"""
+        hooks_string = """
+from zef import *
+from zef.ops import *
+
+@func(g)
+def userCreate(z):
+    z | set_field[RT.HookDidThis][True] | Graph(z) | run
+
+@func(g)
+def dynamicHook(z, info):
+    return "dynamic"
+"""
+
+        from zef.gql.simplegql.main import create_schema_graph
+        from zef.gql.simplegql.server import start_server
+        self.port = 4991
+        root = create_schema_graph(schema_gql, hooks_string)
+        g_data = Graph()
+        self.server_uuid = start_server(root, g_data, self.port, "localhost", logging=False)
+
+    def tearDown(self):
+        Effect({
+            "type": FX.HTTP.StopServer,
+            "server_uuid": self.server_uuid,
+        }) | run
+
+    def test_simplegql(self):
+        import jwt
+        jwt_user_no_aud = jwt.encode({"email": "user1"}, self.key, "HS256")
+        jwt_user1 = jwt.encode({"email": "user1", "aud": "test"}, self.key, "HS256")
+        jwt_user2 = jwt.encode({"email": "user2", "aud": "test"}, self.key, "HS256")
+        jwt_admin = jwt.encode({"email": "", "admin": True, "aud": "test"}, self.key, "HS256")
+
+        import time
+        old_token = jwt.encode({"email": "", "admin": True, "aud": "test", "exp": time.time()-1}, self.key, "HS256")
+
+        import requests
+        url = f"http://localhost:{self.port}/gql"
+
+        boring_query = 'query { queryUser { id } }'
+        def do_query(token, query):
+            return requests.post(url, headers={"X-Auth-Token": "Bearer " + token},
+                                 json={"query": query})
+        def assert_error_with(r, msg):
+            self.assertEqual(r.status_code, 200)
+            self.assertIn("errors", r.json())
+            self.assertIn(msg, r.json()["errors"][0]["message"])
+        def assert_no_error(r):
+            self.assertEqual(r.status_code, 200)
+            self.assertNotIn("errors", r.json())
+
+        # Test aud required
+        r = do_query(jwt_user_no_aud, boring_query)
+        self.assertEqual(r.status_code, 400)
+        r = do_query(jwt_user1, boring_query)
+        assert_no_error(r)
+        self.assertEqual(r.json()["data"]["queryUser"], [])
+
+        # Test expired token
+        r = do_query(old_token, boring_query)
+        self.assertEqual(r.status_code, 400)
+
+        # Test no token in non-public mode
+        r = do_query("", boring_query)
+        self.assertEqual(r.status_code, 400)
+
+        # Test no admin rights
+        r = do_query(jwt_user1, 'mutation { addUser(input: {email: "user1", testEnum: ABC, testString: "test"}) { user { id } count } }')
+        assert_error_with(r, "Add auth check")
+
+        r = do_query(jwt_admin, 'mutation { addUser(input: {email: "user1", testEnum: ABC}) { user { id } count } }')
+        assert_no_error(r)
+        user1_id = r.json()["data"]["addUser"]["user"][0]["id"]
+
+        # Test required input
+        r = do_query(jwt_admin, 'mutation { addUser(input: {email: "user2"}) { user { id } count } }')
+        assert_error_with(r, "Required field")
+
+        # Test getting existing user from correct/incorrect account
+        r = do_query(jwt_user1, 'query { getUser(id: "' + user1_id + '") { id } }')
+        assert_no_error(r)
+
+        r = do_query(jwt_user2, 'query { getUser(id: "' + user1_id + '") { id } }')
+        assert_no_error(r)
+        self.assertIsNone(r.json()["data"]["getUser"])
+
+        # Test adding transaction for auth_field checking
+        r = do_query(jwt_user1, 'mutation { addTransaction(input: {user: {id: "' + user1_id + '"}, amount: 50, date: "' + str(now()) + '"}) { transaction { id } } }')
+        assert_no_error(r)
+        trans_id = r.json()["data"]["addTransaction"]["transaction"][0]["id"]
+
+        r = do_query(jwt_user2, 'query { getTransaction(id: "' + trans_id + '") { id } }')
+        assert_no_error(r)
+        self.assertIsNone(r.json()["data"]["getTransaction"])
+
+        # Test not allowing to change username
+        r = do_query(jwt_user1, 'mutation { updateUser(input: {filter: {id: "' + user1_id + '"}, set: {email: "user2"}}) { user { id } } }')
+        assert_error_with(r, "Post-update auth check")
+
+        r = do_query(jwt_admin, 'mutation { addUser(input: {email: "user2", testEnum: ABC}) { user { id } count } }')
+        assert_no_error(r)
+        user2_id = r.json()["data"]["addUser"]["user"][0]["id"]
+
+        if False:
+            # This needs to be implemented!
+            r = do_query(jwt_user1, 'mutation { updateTransaction(input: {filter: {id: "' + trans_id + '"}, set: {user: {id: "' + user2_id + '"}}}) { transaction { id } } }')
+            assert_error_with(r, "TODO")
+
+        # Test updating and removing things
+        r = do_query(jwt_user1, '''mutation {
+updateUser(input: {filter: {id: "''' + user1_id + '''"},
+set: {
+testTime: "12:00:00"
+},
+remove: {
+testString: ""
+}
+}) { user { id email transactions { id } testString testEnum testOut testTime testDynamic } } }''')
+
+        # Test querying with filters
+        r = do_query(jwt_user1, 'query { queryTransaction(filter: {id: "' + trans_id + '"} ) { id } }')
+        assert_no_error(r)
+        self.assertEqual(len(r.json()["data"]["queryTransaction"]), 1)
+
+        r = do_query(jwt_user1, 'query { queryTransaction(filter: {user: {testEnum: {eq: ABC}, testDynamic: {eq: "dynamic"}}}) { id } }')
+        assert_no_error(r)
+        self.assertEqual(len(r.json()["data"]["queryTransaction"]), 1)
+
+        r = do_query(jwt_user1, 'query { queryTransaction(filter: {user: {testEnum: {eq: DEF}}}) { id } }')
+        assert_no_error(r)
+        self.assertEqual(len(r.json()["data"]["queryTransaction"]), 0)
+
+        r = do_query(jwt_user1, 'query { queryTransaction(filter: {amount: {lt: 1}}) { id } }')
+        assert_no_error(r)
+        self.assertEqual(len(r.json()["data"]["queryTransaction"]), 0)
+
+        r = do_query(jwt_user1, 'query { queryTransaction(filter: {amount: {gt: 1}}) { id } }')
+        assert_no_error(r)
+        self.assertEqual(len(r.json()["data"]["queryTransaction"]), 1)
+
+        r = do_query(jwt_user1, 'query { queryUser(filter: {transactions: {any: {amount: {gt: 1}}}}) { id transactions(filter: {amount: {lt: 1}}) { id } } }')
+        assert_no_error(r)
+        self.assertEqual(len(r.json()["data"]["queryUser"]), 1)
+        self.assertEqual(len(r.json()["data"]["queryUser"][0]["transactions"]), 0)
+
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/python/tests/rem_test_slices.py b/python/tests/test_slices.py
similarity index 84%
rename from python/tests/rem_test_slices.py
rename to python/tests/test_slices.py
index e6606496..2c2654de 100644
--- a/python/tests/rem_test_slices.py
+++ b/python/tests/test_slices.py
@@ -30,14 +30,14 @@ def test_zefrefs_from_slices(self):
 
         ctx = z | instantiation_tx | collect
         # Note: slice 1 is created on Graph(), so this is slice 2
-        self.assertEqual(ctx | to_graph_slice | time_slice | collect, TimeSlice(2))
+        self.assertEqual(ctx | to_graph_slice | graph_slice_index | collect, 2)
 
         self.assertEqual(ctx | events[Instantiated] | length | collect, 1)
         self.assertEqual(ctx | events[Assigned] | length | collect, 0)
         self.assertEqual(ctx | events[events[Terminated]] | length | collect, 0)
 
-        txs = z | now[allow_tombstone] | events[Assigned] | map[absorbed | first | frame | to_tx] | collect
-        self.assertEqual([ctx  | frame | time_slice | collect for ctx in txs], [TimeSlice(3),TimeSlice(4)])
+        txs = z | now[allow_tombstone] | preceding_events[Assigned] | map[absorbed | first | frame | to_tx] | collect
+        self.assertEqual([ctx  | frame | graph_slice_index | collect for ctx in txs], [3, 4])
         for ctx in txs:
             self.assertEqual(ctx | events[Instantiated] | length | collect, 0)
             self.assertEqual(ctx | events[Assigned] | length | collect, 1)
@@ -45,7 +45,7 @@ def test_zefrefs_from_slices(self):
             
 
         ctx = z | now[allow_tombstone] | termination_tx | collect
-        self.assertEqual(ctx | to_graph_slice | time_slice | collect, TimeSlice(5))
+        self.assertEqual(ctx | to_graph_slice | graph_slice_index | collect, 5)
 
         self.assertEqual(ctx | events[Instantiated] | length | collect, 0)
         self.assertEqual(ctx | events[Assigned] | length | collect, 0)
diff --git a/python/tests/test_subscribe.py b/python/tests/test_subscribe.py
index 9083d2fa..1d28db88 100644
--- a/python/tests/test_subscribe.py
+++ b/python/tests/test_subscribe.py
@@ -41,11 +41,11 @@ def OnTerm(x, l):
         sub4 = z | subscribe[on_termination[outgoing][RT.Map]][lambda x: OnTerm(x, on_rel_list)]
         sub5 = z | subscribe[on_termination[outgoing][RT.Value]][lambda x: OnTerm(x, on_rel_list)]
 
-        z | assign_value[1] | g | run
-        z | assign_value[2] | g | run
-        z | assign_value[3] | g | run
-        z | assign_value[4] | g | run
-        z | assign_value[5] | g | run
+        z | assign[1] | g | run
+        z | assign[2] | g | run
+        z | assign[3] | g | run
+        z | assign[4] | g | run
+        z | assign[5] | g | run
 
         (z, RT.Map, 1) | g | run
         (z, RT.Map, 2) | g | run
diff --git a/python/tests/test_to_frame.py b/python/tests/test_to_frame.py
index 536ff0fd..a70e807f 100644
--- a/python/tests/test_to_frame.py
+++ b/python/tests/test_to_frame.py
@@ -28,13 +28,13 @@ def test_do_not_show_terminated_relents_in_Transaction(self):
         m4 = instantiate(ET.Machine, g)
 
         self.assertEqual(m4 | frame | collect, g | now | collect)
-        ts4 = m4 | frame | time_slice | collect
+        ts4 = m4 | frame | graph_slice_index | collect
 
         with Transaction(g) as ctx: 
             this_frame = frame(ctx)
             self.assertEqual(this_frame, g | now | collect)
-            self.assertEqual(ctx | to_graph_slice | time_slice | collect, g | now | time_slice | collect)
-            self.assertEqual(ctx | to_graph_slice | time_slice | func[int] | collect, int(ts4)+1)
+            self.assertEqual(ctx | to_graph_slice | graph_slice_index | collect, g | now | graph_slice_index | collect)
+            self.assertEqual(ctx | to_graph_slice | graph_slice_index | collect, ts4+1)
             
 
         mm1 = m1 | to_ezefref | to_frame[g | now|collect] | collect
diff --git a/python/tests/test_zefref_info.py b/python/tests/test_zefref_info.py
index ecb227bf..aaa731c6 100644
--- a/python/tests/test_zefref_info.py
+++ b/python/tests/test_zefref_info.py
@@ -26,13 +26,13 @@ def test_info(self):
 
         w = instantiate(ET.Worker, g)
 
-        assign_value(f, 'Fritz')
-        assign_value(f, 'Ninja')
+        assign(f, 'Fritz')
+        assign(f, 'Ninja')
 
         instantiate(w, RT.YearOfBirth, f, g)
-        assign_value(f, 'Ninja2')
-        assign_value(f, 'Ninja')
-        assign_value(f, 'Yolandi')
+        assign(f, 'Ninja2')
+        assign(f, 'Ninja')
+        assign(f, 'Yolandi')
 
 
         man = instantiate(ET.Manager, g)