Repository: zefhub/zef (language: python)

Issue:
Simplegql updates



Discussion / hints:

Fix patch:
diff --git a/.DS_Store b/.DS_Store
deleted file mode 100644
index 5008ddfc..00000000
Binary files a/.DS_Store and /dev/null differ
diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 00000000..fb893039
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,2 @@
+python/pyzef/build
+core/build
\ No newline at end of file
diff --git a/.github/actions/build/action.yml b/.github/actions/build/action.yml
index 4100e54d..c707a172 100644
--- a/.github/actions/build/action.yml
+++ b/.github/actions/build/action.yml
@@ -150,21 +150,29 @@ runs:
 
     #########
     # Testing
-    - name: Run python tests
+    - name: Prep for python tests
       if: inputs.run-tests == 'true'
       shell: bash
-      env:
-        ZEFDB_QUIET: YES
-        ZEFHUB_AUTH_KEY: GUEST
       run: |
-        export PYTHONPATH="${GITHUB_WORKSPACE}/python:$PYTHONPATH"
         pip3 install -r python/requirements.txt
         pip3 install -r python/tests/requirements_tests.txt
-        pytest python/tests
 
     - name: Check no created tokens
       if: inputs.run-tests == 'true'
       shell: bash
+      env:
+        ZEFDB_OFFLINE_MODE: TRUE
       run: |
         export PYTHONPATH="${GITHUB_WORKSPACE}/python:$PYTHONPATH"
         bash scripts/assert_no_created_tokens.sh
+
+    - name: Run python tests
+      if: inputs.run-tests == 'true'
+      shell: bash
+      env:
+        ZEFDB_QUIET: YES
+        ZEFHUB_AUTH_KEY: GUEST
+      run: |
+        export PYTHONPATH="${GITHUB_WORKSPACE}/python:$PYTHONPATH"
+        python3 python/tests/quick_start.py
+        python3 -mpytest python/tests
diff --git a/.github/scripts/check-license.sh b/.github/scripts/check-license.sh
old mode 100644
new mode 100755
index a210c93a..20ba76df
--- a/.github/scripts/check-license.sh
+++ b/.github/scripts/check-license.sh
@@ -1,3 +1,3 @@
 #!/bin/bash
 
-addlicense -v -c "Synchronous Technologies Pte Ltd" -f ../assets/apache-license.md -ignore "core/build/**" -ignore "python/pyzef/build/**" -ignore ".github/**" -failhard . 2>&1 | grep -v skipping
+addlicense -v -c "Synchronous Technologies Pte Ltd" -f .github/assets/apache-license.md -ignore "core/build/**" -ignore "python/pyzef/build/**" -ignore ".github/**" -failhard -q . 2>&1
diff --git a/.github/workflows/build-release.yml b/.github/workflows/build-release.yml
index f3a1e0f0..04ad7fbc 100644
--- a/.github/workflows/build-release.yml
+++ b/.github/workflows/build-release.yml
@@ -19,7 +19,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        os: [ubuntu-18.04, macos-latest]
+        os: [ubuntu-20.04, macos-latest]
         # Note: need "3.10" as yaml will truncate floating point number
         # python-version: [3.8, 3.9, "3.10"]
         python-version: ["3.10"]
@@ -38,6 +38,7 @@ jobs:
 
       - name: Build wheel
         uses: ./.github/actions/build
+        timeout-minutes: 15
         with:
           python-version: ${{ matrix.python-version }}
           os: ${{ matrix.os }}
@@ -76,7 +77,7 @@ jobs:
   upload-to-pypi:
     name: Upload wheels to PyPI
     # if: ${{ (!contains(github.ref, 'dev') && github.event.release.prerelease == false) }}
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-20.04
     needs: [build-wheels]
     steps:
       - name: Grab built wheels
@@ -96,7 +97,7 @@ jobs:
           
   update-index:
     name: Update s3 index
-    runs-on: ubuntu-18.04
+    runs-on: ubuntu-20.04
     needs: [build-wheels]
     steps:
       - uses: actions/checkout@v2
diff --git a/.github/workflows/check-license.yml b/.github/workflows/check-license.yml
index 3e3e5309..bb783f10 100644
--- a/.github/workflows/check-license.yml
+++ b/.github/workflows/check-license.yml
@@ -9,5 +9,5 @@ jobs:
       - uses: actions/setup-go@v3
         with:
           go-version: "^1.13.1"
-      - run: go install github.com/zefhub/addlicense@0dff3a5860a17dabe0678454c9ff70b805a2ea5e
+      - run: go install github.com/zefhub/addlicense@18fa4120a13e50674b12c3d36748fc7d84596dad
       - run: bash .github/scripts/check-license.sh
diff --git a/.github/workflows/on-master-merge.yml b/.github/workflows/on-master-merge.yml
index 956617b0..4a5dbe65 100644
--- a/.github/workflows/on-master-merge.yml
+++ b/.github/workflows/on-master-merge.yml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        os: [ubuntu-18.04, macos-latest]
+        os: [ubuntu-20.04, macos-latest]
         python-version: [3.8, 3.9, "3.10"]
         exclude:
           - os: macos-latest
@@ -31,6 +31,7 @@ jobs:
       # TODO: Update this to check via building the wheel rather than a local build
       - name: Build
         uses: ./.github/actions/build
+        timeout-minutes: 15
         with:
           python-version: ${{ matrix.python-version }}
           os: ${{ matrix.os }}
diff --git a/.github/workflows/on-push.yml b/.github/workflows/on-push.yml
index ace65b0e..dfab6757 100644
--- a/.github/workflows/on-push.yml
+++ b/.github/workflows/on-push.yml
@@ -14,7 +14,7 @@ jobs:
     runs-on: ${{ matrix.os }}
     strategy:
       matrix:
-        os: [ubuntu-18.04]
+        os: [ubuntu-20.04]
         python-version: [3.8]
     env:
       ZEFHUB_AUTH_KEY: "GUEST"
@@ -27,6 +27,7 @@ jobs:
 
       - name: Build
         uses: ./.github/actions/build
+        timeout-minutes: 15
         with:
           python-version: ${{ matrix.python-version }}
           os: ${{ matrix.os }}
diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml
index f6ce5193..962940ee 100644
--- a/.github/workflows/pull-request.yml
+++ b/.github/workflows/pull-request.yml
@@ -29,6 +29,7 @@ jobs:
       # TODO: Update this to check via building the wheel rather than a local build
       - name: Build
         uses: ./.github/actions/build
+        timeout-minutes: 15
         with:
           python-version: ${{ matrix.python-version }}
           os: ${{ matrix.os }}
diff --git a/.github/workflows/pyzef-pypi.yml b/.github/workflows/pyzef-pypi.yml
index bcce0e84..6173419a 100644
--- a/.github/workflows/pyzef-pypi.yml
+++ b/.github/workflows/pyzef-pypi.yml
@@ -9,6 +9,7 @@ jobs:
   check-license:
     uses: ./.github/workflows/check-license.yml
   build-wheels:
+    if: ${{ !contains(github.ref, 'dev') }}
     needs: check-license
     name: Build wheels
     runs-on: ${{ matrix.os }}
@@ -40,6 +41,7 @@ jobs:
 
       - name: Build wheels
         uses: pypa/cibuildwheel@v2.4.0
+        timeout-minutes: 20
         with:
           package-dir: python
         env:
@@ -68,6 +70,9 @@ jobs:
   upload-to-pypi:
     name: Upload wheels to PyPI
     runs-on: ubuntu-20.04
+    # We need this if in case build-wheels doesn't run see workflow syntax
+    # documentation for jobs.<job_id>.needs.
+    if: ${{ always() }}
     needs: [build-wheels]
     steps:
       - uses: actions/checkout@v2
@@ -75,6 +80,7 @@ jobs:
           token: ${{ secrets.GITHUB_TOKEN }}
 
       - name: Grab built wheels
+        if: ${{ !contains(github.ref, 'dev') }}
         uses: actions/download-artifact@v3
         with:
           name: wheels
@@ -92,11 +98,14 @@ jobs:
         run: |
           ls -R wheelhouse
           pip3 install --upgrade twine
-          python3 -m twine upload --repository testpypi -u __token__ -p "${PYPI_TOKEN}" --skip-existing wheelhouse/*
+          python3 -m twine upload -u __token__ -p "${PYPI_TOKEN}" --skip-existing wheelhouse/*
           
   dev-announcement:
     name: Dev announcement
     runs-on: ubuntu-20.04
+    # Suprisingly, we need this if always even though upload-to-pypi is always
+    # run, because of the weird dependency handling by the workflow runner.
+    if: ${{ always() }}
     needs: [upload-to-pypi]
     steps:
       - uses: actions/checkout@v2
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 2cda3a2f..1623a525 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -17,3 +17,39 @@ Please don't be shy on submitting a pull request - just dive in!
 ## Your first contribution
 
 A good first place to start contributing is the documentation and tutorials. We feel this is a good way for people who want to get involved to understand Zef and start making meaningful contributions right away!
+
+### Setting up git pre-commit hook
+
+If you are using VS Code you have to un-hide the `.git` folder, this can be done by creating the following config file.
+
+`.vscode/settings.json`
+```json
+{
+    "files.exclude": {
+        "**/.git": false
+   }
+}
+```
+
+The next step is creating the pre-commit file. Notice how the file name has no extention.
+
+`.git/hooks/pre-commit`
+```sh
+#!/bin/bash
+
+# If there are license errors, print the offending file names and fail.
+exec .github/scripts/check-license.sh 2>&1
+```
+
+Next we need to enable execution.
+```sh
+chmod +x .git/hooks/pre-commit
+```
+
+Finally we need to install the `addlicense` tool, please note that you need to have [Go Lang]([https://go.dev/) installed on your system.
+
+```sh
+go install github.com/zefhub/addlicense@18fa4120a13e50674b12c3d36748fc7d84596dad
+```
+
+Now you are ready to go, the hook should run automaticly before every commit.
diff --git a/INSTALL.md b/INSTALL.md
index 0da4ea90..4058c7f5 100644
--- a/INSTALL.md
+++ b/INSTALL.md
@@ -4,6 +4,12 @@ Simply run:
 
 `pip install zef`
 
+You may have to replace `pip` with `pip3` in some distributions.
+
+Native Windows installs are currently not supported, although it is possible to
+install Zef in a WSL (Windows Subsystem for Linux) environment. We are aiming to
+include native Windows support soon.
+
 # Compiling from source
 
 Either obtain the source distribution from PyPI using:
@@ -23,7 +29,6 @@ and then
 Compiling from source requires the following system libraries:
 
 - OpenSSL and headers
-- libcurl and headers
 
 and the following build-time python libraries:
 
@@ -40,4 +45,7 @@ A convenience script exists in the repo root:
 
 which compiles both libzef and the python bindings, along with including a symlink to the library in the source repo. Adding the `<repo_root>/python` path to `PYTHONPATH` will then allow `import zef` to find the package.
 
-TODO more details.
+You may need to manually include all of the python requirements for Zef by
+running:
+
+`pip install -r python/requirements.txt`
diff --git a/README.md b/README.md
index 8b74aeaa..d0f1da7a 100644
--- a/README.md
+++ b/README.md
@@ -210,3 +210,19 @@ Please refer to our [CONTRIBUTING file](https://github.com/zefhub/zef/blob/maste
 Zef is licensed under the Apache License, Version 2.0 (the "License"). You may obtain a copy of the License at
 
 [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)
+
+## Dependencies
+
+The compiled libraries make use of the following packages:
+
+- `asio` (https://github.com/chriskohlhoff/asio)
+- `JWT++` (https://github.com/Thalhammer/jwt-cpp)
+- `Curl` (https://github.com/curl/curl)
+- `JSON`  (https://github.com/nlohmann/json)
+- `Parallel hashmap` (https://github.com/greg7mdp/parallel-hashmap)
+- `Ranges-v3` (https://github.com/ericniebler/range-v3)
+- `Websocket++` (https://github.com/zaphoyd/websocketpp)
+- `Zstandard` (https://github.com/facebook/zstd)
+- `pybind11` (https://github.com/pybind/pybind11)
+- `pybind_json` (https://github.com/pybind/pybind11_json)
+
diff --git a/core/scripts/setup_tools_prep.sh b/add_licenses.sh
similarity index 63%
rename from core/scripts/setup_tools_prep.sh
rename to add_licenses.sh
index 0a2f27b2..4ededd35 100644
--- a/core/scripts/setup_tools_prep.sh
+++ b/add_licenses.sh
@@ -14,15 +14,4 @@
 # limitations under the License.
 
 
-set -e
-
-echo $(git rev-parse HEAD) > zef/packaged_git_commit
-
-(cd cmake_build && make install)
-
-# Need to make sure that the zeftypes are in the zefdb folder for fallbacks.
-cp templates/zeftypes_bootstrap_??.json zef/
-cp zeftypes_??.json zef/
-cp cmake_install/lib/libzef* zef
-cp cmake_install/lib/pyzef* zef
-cp cmake_install/share/zefDB/auth.html zef
\ No newline at end of file
+addlicense -c "Synchronous Technologies Pte Ltd" -f .github/assets/apache-license.md -ignore "core/build/**" -ignore "python/pyzef/build/**" -ignore ".github/**" -q .
diff --git a/compile_for_local_dev.sh b/compile_for_local_dev.sh
index e6a7e4f9..af346856 100755
--- a/compile_for_local_dev.sh
+++ b/compile_for_local_dev.sh
@@ -13,8 +13,8 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
+echo 'This command uses several pip-installed binaries. Make sure that the pip-installed binary path is on your PATH environment variable. For example, PATH=$PATH:$HOME/.local/bin'
 
-echo "pip-ing"
 if ! which jq > /dev/null ; then
     echo 'jq needs to be installed to use this script. If you are on macos, please run `brew install jq`. If you are on linux use your package manager to install jq (e.g. `sudo apt-get install jq` or `sudo pacman -S jq`)'
     exit 1
@@ -59,7 +59,7 @@ python3 -m pip install "${packages[@]}" || exit 1
         CMAKE_ARGS="-j $np"
     fi
     # The -DPython3_EXECUTABLE seems necessary here for github CI
-    cmake .. -DCMAKE_BUILD_TYPE=Release -DCMAKE_PREFIX_PATH=$(realpath ../../../core) -DPython3_EXECUTABLE=$(which python3) || exit 1
+    cmake .. -DCMAKE_BUILD_TYPE=${CMAKE_BUILD_TYPE:-Release} -DCMAKE_PREFIX_PATH=$(realpath ../../../core) -DPython3_EXECUTABLE=$(which python3) || exit 1
 
     cmake --build . $CMAKE_ARGS || exit 1
 ) || exit 1
diff --git a/core/CMakeLists.txt b/core/CMakeLists.txt
index 8a77c43b..eb1595af 100644
--- a/core/CMakeLists.txt
+++ b/core/CMakeLists.txt
@@ -186,13 +186,15 @@ endif()
 if(LIBZEF_PYZEF_BUNDLED)
   install(TARGETS zef LIBRARY DESTINATION .)
   install(FILES auth.html DESTINATION .)
+  install(FILES ${LICENSE_FILES} DESTINATION .)
 else()
   install ( TARGETS zef
     EXPORT zef
     LIBRARY DESTINATION lib
     PUBLIC_HEADER DESTINATION include/zef
     )
-  install(FILES auth.html DESTINATION share/zef)
+    install(FILES auth.html DESTINATION share/zef)
+    install(FILES ${LICENSE_FILES} DESTINATION share/zef)
 endif()
 
 ############################
diff --git a/core/external/external.cmake b/core/external/external.cmake
index 513234e2..12b39a3e 100644
--- a/core/external/external.cmake
+++ b/core/external/external.cmake
@@ -40,6 +40,59 @@ if (APPLE)
 endif()
 
 
+######################
+# * Setup for the upstream pulling
+
+set(LICENSE_DIR ${CMAKE_CURRENT_BINARY_DIR}/licenses)
+if(LIBZEF_PYZEF_BUNDLED)
+  set(TOP_LICENSE_FILE "${LICENSE_DIR}/LICENSE.libzef")
+else()
+  set(TOP_LICENSE_FILE "${LICENSE_DIR}/LICENSE")
+endif()
+set(LICENSE_FILES ${TOP_LICENSE_FILE})
+
+# Initialise the top file - which could be in different places if we are being compiled from pyzef sdist
+find_file(REPO_LICENSE
+  LICENSE
+  PATHS ${CMAKE_CURRENT_SOURCE_DIR} ${CMAKE_CURRENT_SOURCE_DIR}/../
+  NO_DEFAULT_PATH
+  NO_CACHE
+  REQUIRED)
+  
+file(READ ${REPO_LICENSE} OUR_LICENSE)
+file(WRITE ${TOP_LICENSE_FILE} ${OUR_LICENSE})
+
+file(APPEND ${TOP_LICENSE_FILE} "\nThe following files contain additional bundled licenses:\n\n")
+
+###########
+function(create_license_file SHORT_NAME LICENSE_FILE HAS_COPYING PREAMBLE)
+  file(READ ${LICENSE_FILE} CONTENTS)
+  file(WRITE ${LICENSE_DIR}/LICENSE.${SHORT_NAME} ${PREAMBLE})
+  file(APPEND ${LICENSE_DIR}/LICENSE.${SHORT_NAME} ${CONTENTS})
+
+  list(APPEND LICENSE_FILES ${LICENSE_DIR}/LICENSE.${SHORT_NAME})
+  # Add to the top file
+  file(APPEND ${TOP_LICENSE_FILE} "LICENSE.${SHORT_NAME}\n")
+
+  get_filename_component(SEARCH_DIR ${LICENSE_FILE} DIRECTORY)
+  if(HAS_COPYING)
+    # Note: copying a file only works when not renaming, (otherwise it creates a
+    # directory) and the COPY_FILE variant is only avaiable in version 3.21, so
+    # instead we cheat with the configure_file command.
+    # file(COPY ${SEARCH_DIR}/COPYING DESTINATION ${LICENSE_DIR}/COPYING.${SHORT_NAME})
+    configure_file(${SEARCH_DIR}/COPYING ${LICENSE_DIR}/COPYING.${SHORT_NAME} COPYONLY)
+    list(APPEND LICENSE_FILES ${LICENSE_DIR}/COPYING.${SHORT_NAME})
+  endif()
+
+  if(EXISTS ${SEARCH_DIR}/NOTICE)
+    message(fatal_error "A NOTICE file was found that we haven't taken care of!")
+  endif()
+
+  # Variables need to be propagated back up
+  set(LICENSE_FILES ${LICENSE_FILES} PARENT_SCOPE)
+endfunction()
+##########
+
 include(FetchContent)
 # set(FETCHCONTENT_QUIET OFF)
 # get_filename_component(FETCHCONTENT_BASE_DIR ${CMAKE_BINARY_DIR}/../_cmake_deps REALPATH)
@@ -51,6 +104,8 @@ function(ManualFetchContent_MakeAvailable name)
   endif()
 
   add_subdirectory(${${name}_SOURCE_DIR} ${${name}_BINARY_DIR} EXCLUDE_FROM_ALL)
+  # I don't understand but somehow rangesv3 (and only rangesv3) prevents us for propagating the source dir up higher
+  set(LAST_SOURCE_DIR ${${name}_SOURCE_DIR} PARENT_SCOPE)
 endfunction()
 
 find_package(PkgConfig QUIET)
@@ -134,6 +189,7 @@ FetchContent_Declare(nlohmann_json
 ManualFetchContent_MakeAvailable(nlohmann_json)
 # set(nlohmann_json_DIR ${nlohmann_json_SOURCE_DIR} CACHE PATH "" FORCE)
 
+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")
 
 # * asio
 
@@ -156,6 +212,8 @@ add_library(asio INTERFACE IMPORTED)
 target_include_directories(asio SYSTEM INTERFACE ${asio_SOURCE_DIR}/asio/include)
 target_link_libraries(asio INTERFACE openssl)
 
+create_license_file("asio" ${asio_SOURCE_DIR}/asio/LICENSE_1_0.txt YES "This library bundles the non-Boost ASIO networking library (https://github.com/chriskohlhoff/asio)\n\nThe full text of the ASIO license follows below.\n\n")
+
 # * websocketpp
 message(STATUS "External: websocketpp")
 
@@ -176,6 +234,8 @@ add_library(websocketpp INTERFACE IMPORTED)
 target_include_directories(websocketpp SYSTEM INTERFACE ${websocketpp_SOURCE_DIR})
 target_link_libraries(websocketpp INTERFACE asio)
 
+create_license_file("websocketpp" ${websocketpp_SOURCE_DIR}/COPYING NO "This library bundles the websocketpp library (https://github.com/chriskohlhoff/asio)\n\nThe full text of the websocketpp license follows below.\n\n")
+
 
 # * parallel-hashmap
 message(STATUS "External: parallel-hashmap")
@@ -189,6 +249,8 @@ FetchContent_Declare(phmap
 
 ManualFetchContent_MakeAvailable(phmap)
 
+create_license_file("phmap" ${phmap_SOURCE_DIR}/LICENSE NO "This library bundles the parallel-hashmap library (https://github.com/greg7mdp/parallel-hashmap)\n\nThe full text of the phmap license follows below.\n\n")
+
 # * doctest
 message(STATUS "External: doctest")
 
@@ -211,6 +273,8 @@ FetchContent_Declare(rangesv3
 
 ManualFetchContent_MakeAvailable(rangesv3)
 
+create_license_file("rangesv3" ${LAST_SOURCE_DIR}/LICENSE.txt NO "This library bundles the ranges-v3 library (https://github.com/ericniebler/range-v3)\n\nThe full text of the ranges-v3 license follows below.\n\n")
+
 # # * blake3
 # message(STATUS "External: blake3")
 # FetchContent_Declare(blake3
@@ -237,6 +301,7 @@ ManualFetchContent_MakeAvailable(rangesv3)
 #     )
 
 # target_include_directories(blake3 SYSTEM INTERFACE ${blake3_SOURCE_DIR}/c)
+# create_license_file("blake3" ${blake3_SOURCE_DIR}/LICENSE NO "This library bundles the blake3 library (https://github.com/BLAKE3-team/BLAKE3)\n\nThe full text of the license follows below.\n\n")
 
 # * jwt-cpp
 message(STATUS "External: jwt-cpp")
@@ -253,6 +318,8 @@ set(JWT_BUILD_EXAMPLES OFF CACHE BOOL "")
 
 ManualFetchContent_MakeAvailable(jwt-cpp)
 
+create_license_file("jwt-cpp" ${jwt-cpp_SOURCE_DIR}/LICENSE NO "This library bundles the JWT++ library (https://github.com/Thalhammer/jwt-cpp)\n\nThe full text of the jwt-cpp license follows below.\n\n")
+
 
 # * zstd
 if(LIBZEF_BUNDLED_ZSTD)
@@ -310,6 +377,8 @@ else()
     add_library(libzstd_internal INTERFACE)
     target_include_directories(libzstd_internal SYSTEM INTERFACE ${ZSTD_INCLUDE_DIRS})
     target_link_libraries(libzstd_internal INTERFACE ${ZSTD_LIBRARIES})
+
+    create_license_file("zstd" ${zstd_SOURCE_DIR}/LICENSE NO "This library bundles the Zstandard library (https://github.com/facebook/zstd)\n\nThe full text of the zstd license follows below.\n\n")
   else()
     message(FATAL_ERROR "Couldn't find zstd via cmake, pkg-config or find_library")
   endif()
@@ -349,6 +418,8 @@ if(LIBZEF_BUNDLED_CURL)
   add_subdirectory(${curl_SOURCE_DIR} ${curl_BINARY_DIR} EXCLUDE_FROM_ALL)
   set_target_properties(libcurl PROPERTIES
     POSITION_INDEPENDENT_CODE ON)
+
+  create_license_file("libcurl" ${curl_SOURCE_DIR}/COPYING NO "This library bundles the libcurl library (https://github.com/curl/curl)\n\nThe full text of the libcurl license follows below.\n\n")
 else()
   if(PKGCONFIG_FOUND)
     pkg_check_modules(CURL libcurl)
@@ -380,5 +451,3 @@ else()
 endif()
 
 
-
-
diff --git a/core/include/butler/butler.h b/core/include/butler/butler.h
index 96a9a6d1..56af757a 100644
--- a/core/include/butler/butler.h
+++ b/core/include/butler/butler.h
@@ -416,7 +416,8 @@ namespace zefDB {
         // * External handlers
 
         typedef json (merge_handler_t)(Graph, const json &);
-        LIBZEF_DLL_EXPORTED void register_merge_handler(merge_handler_t func);
+        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);
 
 
diff --git a/core/include/butler/messages.h b/core/include/butler/messages.h
index 53a5b5e8..24105760 100644
--- a/core/include/butler/messages.h
+++ b/core/include/butler/messages.h
@@ -165,7 +165,7 @@ namespace zefDB {
         struct MergeRequest {
             struct PayloadGraphDelta {
                 // TODO: Currently this is got from python as a python object, but will later be a native C object.
-                json delta;
+                json commands;
             };
             // If task_uid is not set, then this has been generated locally
             std::optional<std::string> task_uid;
diff --git a/core/include/high_level_api.h b/core/include/high_level_api.h
index 139c7ab2..b40b5764 100644
--- a/core/include/high_level_api.h
+++ b/core/include/high_level_api.h
@@ -1804,6 +1804,8 @@ namespace zefDB {
 			case BT.ENTITY_NODE: return true;
 			case BT.ATOMIC_ENTITY_NODE: return true;
 			case BT.RELATION_EDGE: return true;
+			case BT.TX_EVENT_NODE: return true;
+			case BT.ROOT_NODE: return true;
 			default: return false;
 			};
 			return false;
@@ -2212,7 +2214,7 @@ namespace zefDB {
                                  
                                  
 		// called when creating a relation: prevent the user from linking any blobs_ns other than these
- 		inline auto assert_is_this_an_entity_or_relation = [](EZefRef uzr) {
+ 		inline auto assert_is_this_a_rae = [](EZefRef uzr) {
 			switch (*(BlobType*)uzr.blob_ptr) {
 			case BlobType::ENTITY_NODE: return true;
 			case BlobType::ATOMIC_ENTITY_NODE: return true;
@@ -2223,11 +2225,13 @@ namespace zefDB {
                                  
                                  
 		// called when creating a relation: prevent the user from linking any blobs_ns other than these
- 		inline auto assert_target_node_can_be_linked_via_relation = [](EZefRef uzr) {
+ 		inline auto assert_blob_can_be_linked_via_relation = [](EZefRef uzr) {
 			switch (*(BlobType*)uzr.blob_ptr) {
 			case BlobType::ENTITY_NODE: return true;
 			case BlobType::ATOMIC_ENTITY_NODE: return true;
 			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");
 			}                    
 		};                       
diff --git a/core/scripts/.gitignore b/core/scripts/.gitignore
deleted file mode 100644
index 1f314508..00000000
--- a/core/scripts/.gitignore
+++ /dev/null
@@ -1,1 +0,0 @@
-*.tokens
diff --git a/core/scripts/Dockerfile.build_wheel b/core/scripts/Dockerfile.build_wheel
deleted file mode 100644
index 1cb41977..00000000
--- a/core/scripts/Dockerfile.build_wheel
+++ /dev/null
@@ -1,34 +0,0 @@
-FROM pengwyn/julia-python-base:jl1.5-py3.8
-
-ARG CODEARTIFACT_TOKEN
-ARG GIT_CREDENTIALS
-ARG ZEFHUB_AUTH_KEY
-
-ARG VERSION_STRING
-ARG WITH_JULIA
-
-RUN echo "Building with version string $VERSION_STRING and WITH_JULIA=$WITH_JULIA"
-
-# RUN [ -n "$CODEARTIFACT_TOKEN" -a -n "$GIT_CREDENTIALS" -a -n "$VERSION_STRING" ]
-RUN [ -n "$CODEARTIFACT_TOKEN" -a -n "$GIT_CREDENTIALS" ]
-
-ENV TWINE_USERNAME=aws
-ENV TWINE_PASSWORD=$CODEARTIFACT_TOKEN
-ENV TWINE_REPOSITORY_URL=https://aws:$CODEARTIFACT_TOKEN@synchronous-385811086788.d.codeartifact.ap-southeast-1.amazonaws.com/pypi/custom/
-
-RUN git config --global --add credential.helper store \
-    && git config --global url."https://github.com/".insteadof git@github.com \
-    && echo $GIT_CREDENTIALS > ~/.git-credentials
-
-# Put this here, since it can take forever to grab everything
-RUN pip3 install twine
-
-COPY . .
-
-RUN jlpkg registry add https://github.com/synchronoustechnologies/JuliaRegistry
-
-RUN bash make_cleanslate.sh
-
-RUN bash setup_tools_wrapper.sh
-
-RUN twine upload -r codeartifact dist/*
diff --git a/core/scripts/zz_build_wheel_py39.sh b/core/scripts/zz_build_wheel_py39.sh
deleted file mode 100644
index 91447f86..00000000
--- a/core/scripts/zz_build_wheel_py39.sh
+++ /dev/null
@@ -1,31 +0,0 @@
-#!/bin/bash
-# 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.
-
-
-if [ -z "$GIT_CREDENTIALS" -a -f ~/.git-credentials ] ; then
-    echo "Grabbing git credentials from ~/.git-credentials"
-    export GIT_CREDENTIALS=$(cat ~/.git-credentials)
-fi
-export CODEARTIFACT_TOKEN=$(aws codeartifact get-authorization-token --domain synchronous --query authorizationToken --output text)
-
-sed -e 's/julia-python-base:jl1.5-py3.8/julia-python-base:jl1.5-py3.9/' Dockerfile.build_wheel > Dockerfile.build_wheel.py39 || exit 1
-
-docker build -f Dockerfile.build_wheel.py39 \
-       --build-arg CODEARTIFACT_TOKEN \
-       --build-arg GIT_CREDENTIALS \
-       --build-arg VERSION_STRING \
-       --build-arg ZEFHUB_AUTH_KEY \
-       --build-arg WITH_JULIA \
-       . "$@"
diff --git a/core/scripts/zz_only_if_gh_breaks_build_wheel.sh b/core/scripts/zz_only_if_gh_breaks_build_wheel.sh
deleted file mode 100755
index 792ad0c2..00000000
--- a/core/scripts/zz_only_if_gh_breaks_build_wheel.sh
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/bin/bash
-# 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.
-
-
-if [ -z "$VERSION_STRING" ] ; then
-    export VERSION_STRING=$(git tag --points-at HEAD)
-    if [[ -z "$VERSION_STRING" ]] ; then
-        echo "Git HEAD doesn't have a tag - either set one or specify VERSION_STRING manually"
-        exit 1
-    fi
-    echo "Automatic version $VERSION_STRING found"
-fi
-
-if [ -z "$GIT_CREDENTIALS" -a -f ~/.git-credentials ] ; then
-    echo "Grabbing git credentials from ~/.git-credentials"
-    export GIT_CREDENTIALS=$(cat ~/.git-credentials)
-fi
-export CODEARTIFACT_TOKEN=$(aws codeartifact get-authorization-token --domain synchronous --query authorizationToken --output text)
-
-docker build -f Dockerfile.build_wheel \
-       --build-arg CODEARTIFACT_TOKEN \
-       --build-arg GIT_CREDENTIALS \
-       --build-arg ZEFHUB_AUTH_KEY \
-       --build-arg VERSION_STRING \
-       --build-arg WITH_JULIA \
-       . "$@"
diff --git a/core/src/butler/butler.cpp b/core/src/butler/butler.cpp
index ba0c3583..5a2e2e7a 100644
--- a/core/src/butler/butler.cpp
+++ b/core/src/butler/butler.cpp
@@ -111,7 +111,7 @@ namespace zefDB {
 
             debug_time_print("initialise butler");
 
-            if(!butler_is_master && check_env_bool("ZEF_OFFLINE_MODE")) {
+            if(!butler_is_master && check_env_bool("ZEFDB_OFFLINE_MODE")) {
                 initialise_butler_as_master();
                 return;
             }
@@ -1593,7 +1593,7 @@ namespace zefDB {
                 // We have a special spam message in here to let users know what's going on. 
                 wait_pred(auth_locker, done_auth, std::chrono::seconds(3));
                 if(!done_auth()) {
-                    std::cerr << "Warning: waiting for connection with ZefHub is taking a long time.\n\nIf you would like to see more information enable debug messages through `zwitch.zefhub_communication_output(True)` or setting the environment variable `ZEFDB_VERBOSE=true`.\n\nIf you would like to run Zef in offline mode, then start a new python session with the environment variable `ZEF_OFFLINE_MODE=TRUE`." << std::endl;
+                    std::cerr << "Warning: waiting for connection with ZefHub is taking a long time.\n\nIf you would like to see more information enable debug messages through `zwitch.zefhub_communication_output(True)` or setting the environment variable `ZEFDB_VERBOSE=true`.\n\nIf you would like to run Zef in offline mode, then start a new python session with the environment variable `ZEFDB_OFFLINE_MODE=TRUE`." << std::endl;
                     wait_pred(auth_locker, done_auth);
                 }
             }
@@ -1605,7 +1605,7 @@ namespace zefDB {
             if(no_credentials_warning)
                 throw std::runtime_error("No credentials present to allow auth to take place.");
             if(fatal_connection_error)
-                throw std::runtime_error("Fatal connection error while trying to auth with ZefHub.\n\nIf you would like to run in offline mode, please restart your python session with the environment variable `ZEF_OFFLINE_MODE=TRUE`.");
+                throw std::runtime_error("Fatal connection error while trying to auth with ZefHub.\n\nIf you would like to run in offline mode, please restart your python session with the environment variable `ZEFDB_OFFLINE_MODE=TRUE`.");
             debug_time_print("finish wait_for_auth");
             return true;
         }
@@ -2029,10 +2029,16 @@ namespace zefDB {
             return (*merge_handler)(g, payload);
         }
 
-        void register_merge_handler(merge_handler_t func) {
+        void register_merge_handler(std::function<merge_handler_t> func) {
             if(merge_handler)
                 throw std::runtime_error("Merge handler has already been registered.");
             merge_handler = func;
         }
+
+        void remove_merge_handler() {
+            if(!merge_handler)
+                std::cerr << "Warning, no merge_handler registered to be removed." << std::endl;
+            merge_handler.reset();
+        }
     }
 }
diff --git a/core/src/butler/butler_handlers_graph_manager.cpp b/core/src/butler/butler_handlers_graph_manager.cpp
index a578cb88..a672fd1c 100644
--- a/core/src/butler/butler_handlers_graph_manager.cpp
+++ b/core/src/butler/butler_handlers_graph_manager.cpp
@@ -728,16 +728,13 @@ void Butler::graph_worker_handle_message(Butler::GraphTrackingData & me, NotifyS
             //     msg->promise.set_value(GenericResponse{false, "Timed out waiting for sync."});
             //     return;
             // }
-            std::cerr << "Sync is goign to wait_pred for sync_head to come up to date." << std::endl;
             // We use poll here to bail out when network disconnection happens.
             // It would be preferable to listen to two CVs simultaneously, but
             // that is not natively supported. Maybe a rewrite of the locks
             // structure would help here.
             wait_pred_poll(me.gd->heads_locker,
                       [&]() {
-                          std::cerr << "Wait_pred test has sync_head: " << me.gd->sync_head.load() << " and sync_to: " << sync_to << std::endl;
                           return me.gd->sync_head >= sync_to || !network.connected || me.gd->error_state != GraphData::ErrorState::OK; }, std::chrono::seconds(1));
-            std::cerr << "Sync after wait_pred for sync_head to come up to date." << std::endl;
 
             if(!network.connected) {
                 msg->promise.set_value(GenericResponse{false, "Lost network connection while trying to sync."});
@@ -852,7 +849,7 @@ void Butler::graph_worker_handle_message(Butler::GraphTrackingData & me, MergeRe
                     // py::gil_scoped_acquire acquire;
                     // auto pymerge = py::module_::import("zef.core.internals.merges").attr("_graphdelta_merge");
                     // json receipt = pymerge(Graph(*me.gd), payload.delta);
-                    json receipt = pass_to_merge_handler(Graph(*me.gd), payload.delta);
+                    json receipt = pass_to_merge_handler(Graph(*me.gd), payload.commands);
 
                     if(content.task_uid) {
                         if(content.msg_version <= 0) {
diff --git a/core/src/butler/butler_handlers_main.cpp b/core/src/butler/butler_handlers_main.cpp
index 7a8f0ae3..3c38a54d 100644
--- a/core/src/butler/butler_handlers_main.cpp
+++ b/core/src/butler/butler_handlers_main.cpp
@@ -73,7 +73,7 @@ void Butler::handle_guest_message(TokenQuery & content, Butler::msg_ptr & msg) {
         if(content.group == TokenQuery::KW && !zwitch.allow_dynamic_keyword_definitions())
             throw std::runtime_error("KW creation is disallowed.");
 
-        if(check_env_bool("ZEF_OFFLINE_MODE") || check_env_bool("ZEFDB_DEVELOPER_LOCAL_TOKENS")) {
+        if(check_env_bool("ZEFDB_OFFLINE_MODE") || check_env_bool("ZEFDB_DEVELOPER_LOCAL_TOKENS")) {
             auto & tokens = global_token_store();
             std::vector<TokenQueryResponse::pair> pairs;
             for(auto & name : content.names) {
@@ -108,7 +108,7 @@ void Butler::handle_guest_message(TokenQuery & content, Butler::msg_ptr & msg) {
 
     // This is a bit of the logic inside of wait_for_auth, so that we can put a more informative error message.
     if(!want_upstream_connection())
-        throw std::runtime_error("We can't create new tokens unless we can connect to ZefHub. Either make sure you have logged in using `login | run` to store your credentials, or run in offline mode by restarting your python session with the environment variable `ZEF_OFFLINE_MODE=TRUE` set.");
+        throw std::runtime_error("We can't create new tokens unless we can connect to ZefHub. Either make sure you have logged in using `login | run` to store your credentials, or run in offline mode by restarting your python session with the environment variable `ZEFDB_OFFLINE_MODE=TRUE` set.");
 
     wait_for_auth();
 
@@ -356,7 +356,7 @@ void Butler::handle_guest_message(MergeRequest & content, Butler::msg_ptr & msg)
                                 {"target_guid", str(content.target_guid)},
                                 {"payload", {
                                         {"type", "delta"},
-                                        {"delta", payload.delta}
+                                        {"commands", payload.commands}
                                     }}
                             });
                     }
diff --git a/core/src/butler/butler_handlers_ws.cpp b/core/src/butler/butler_handlers_ws.cpp
index 89dd9676..ba004d12 100644
--- a/core/src/butler/butler_handlers_ws.cpp
+++ b/core/src/butler/butler_handlers_ws.cpp
@@ -21,6 +21,9 @@ void handle_token_response(Butler & butler, json & j, Butler::task_promise_ptr &
 
 
 void Butler::ws_open_handler(void) {
+    if(should_stop)
+        return;
+
     // Reset any variables that need it.
     zefdb_protocol_version = -1;
     // connection_authed = false;
@@ -36,6 +39,9 @@ void Butler::ws_open_handler(void) {
 }
 
 void Butler::ws_close_handler(void) {
+    if(should_stop)
+        return;
+
     // TODO: Tell any graph managers that we lost the connection.
     // Note: this means we should cancel any tasks that rely on
     // network. This means we need to tag those tasks as being
@@ -52,7 +58,7 @@ void Butler::ws_close_handler(void) {
 
 void Butler::ws_fatal_handler(std::string reason) {
     std::cerr << "FATAL: connection failure in background WS thread, reason: " << reason << std::endl;
-    std::cerr << "If you would like to run in offline mode instead, please restart your python session and set the environment variable `ZEF_OFFLINE_MODE=TRUE`." << std::endl;
+    std::cerr << "If you would like to run in offline mode instead, please restart your python session and set the environment variable `ZEFDB_OFFLINE_MODE=TRUE`." << std::endl;
     update(auth_locker, fatal_connection_error, true);
 
     ws_close_handler();
@@ -60,6 +66,9 @@ void Butler::ws_fatal_handler(std::string reason) {
 
 
 void Butler::ws_message_handler(std::string msg) {
+    if(should_stop)
+        return;
+
     auto [j, rest]  = Communication::parse_ZH_message(msg);
 
     if(zwitch.debug_zefhub_json_output())
@@ -429,7 +438,7 @@ void Butler::handle_incoming_merge_request(json & j) {
                 task_uid,
                 target_guid,
                 MergeRequest::PayloadGraphDelta{
-                    j["payload"]["delta"].get<json>(),
+                    j["payload"]["commands"].get<json>(),
                 },
                 msg_version
             };
diff --git a/core/src/butler/communication.cpp b/core/src/butler/communication.cpp
index 40071b94..e2c9e17d 100644
--- a/core/src/butler/communication.cpp
+++ b/core/src/butler/communication.cpp
@@ -327,6 +327,12 @@ namespace zefDB {
                     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
@@ -339,16 +345,19 @@ namespace zefDB {
             std::visit([this,failure](auto & con) {
 #endif
                 should_stop = true;
-                if(!con)
-                    return;
-
-                // 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(con) {
+                    std::error_code ec;
+                    con->close(websocketpp::close::status::going_away, "", ec);
+                }
 
-                std::error_code ec;
-                con->close(websocketpp::close::status::going_away, "", ec);
+                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
@@ -494,6 +503,12 @@ namespace zefDB {
 #if ZEFDB_ALLOW_NO_TLS
                 }, endpoint);
 #endif
+                // 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
+                // halfway through its thing)
+                close();
+
                 ws_thread->join();
                 ws_thread.reset();
             }
diff --git a/core/src/high_level_api.cpp b/core/src/high_level_api.cpp
index ca771ae9..bd321642 100644
--- a/core/src/high_level_api.cpp
+++ b/core/src/high_level_api.cpp
@@ -2209,7 +2209,7 @@ namespace zefDB {
 
 
 		bool is_terminated(EZefRef my_rel_ent) {
- 			assert_is_this_an_entity_or_relation(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));
@@ -2314,8 +2314,8 @@ namespace zefDB {
 		// tasks::apply_immediate_updates_from_zm();
 		using namespace internals;
 		auto my_tx_to_keep_this_open_in_this_fct = Transaction(gd);
- 		assert_is_this_an_entity_or_relation(my_source);
- 		assert_target_node_can_be_linked_via_relation(my_target);
+ 		assert_blob_can_be_linked_via_relation(my_source);
+ 		assert_blob_can_be_linked_via_relation(my_target);
 
 
 
@@ -3361,7 +3361,7 @@ namespace zefDB {
             // tasks::apply_immediate_updates_from_zm();
             // TODO: check whether locally owned!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
             using namespace internals;
-            assert_is_this_an_entity_or_relation(my_rel_ent);
+            assert_is_this_a_rae(my_rel_ent);
             if (is_terminated(my_rel_ent))
                 throw std::runtime_error("Terminate called on already terminated entity or relation.");
 
@@ -3567,7 +3567,8 @@ namespace zefDB {
         }
 
         std::optional<EZefRef> delegate_to_ezr(const DelegateTX & d, int order, Graph g, bool create) {
-            EZefRef root{constants::ROOT_NODE_blob_index, g.my_graph_data()};
+            auto & gd = g.my_graph_data();
+            EZefRef root{constants::ROOT_NODE_blob_index, gd};
 
             EZefRef z = root;
             for(int i = 0 ; i < order ; i++) {
@@ -3575,18 +3576,23 @@ namespace zefDB {
                                        [&](EZefRef z) { return BT(z) == BT.TX_EVENT_NODE; });
                 if(length(opts) == 0) {
                     if(create) {
-                        throw std::runtime_error("Create not implemented yet");
+                        EZefRef tx = internals::get_or_create_and_get_tx(gd);
+                        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);
                     } else
                         return {};
+                } else {
+                    z = only(opts);
                 }
-                z = only(opts);
             }
 
             return z;
         }
 
         std::optional<EZefRef> delegate_to_ezr(const DelegateRoot & d, int order, Graph g, bool create) {
-            EZefRef root{constants::ROOT_NODE_blob_index, g.my_graph_data()};
+            auto & gd = g.my_graph_data();
+            EZefRef root{constants::ROOT_NODE_blob_index, gd};
 
             EZefRef z = root;
             for(int i = 0 ; i < order ; i++) {
@@ -3594,11 +3600,15 @@ namespace zefDB {
                                        [&](EZefRef z) { return BT(z) == BT.ROOT_NODE; });
                 if(length(opts) == 0) {
                     if(create) {
-                        throw std::runtime_error("Create not implemented yet");
+                        EZefRef tx = internals::get_or_create_and_get_tx(gd);
+                        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);
                     } else
                         return {};
+                } else {
+                    z = only(opts);
                 }
-                z = only(opts);
             }
 
             return z;
diff --git a/dockerfiles/Dockerfile.archlinux b/dockerfiles/Dockerfile.archlinux
index 01327b62..7b3f0f36 100644
--- a/dockerfiles/Dockerfile.archlinux
+++ b/dockerfiles/Dockerfile.archlinux
@@ -9,8 +9,9 @@ RUN pacman -S --noconfirm \
     curl \
     zstd \
     openssl \
-    git
+    git \
+    jq
 
 COPY . .
 
-RUN LIBZEF_LOCATION=$(realpath core) pip3 install ./python
\ No newline at end of file
+RUN LIBZEF_LOCATION=$(realpath core) pip3 install ./python
diff --git a/dockerfiles/Dockerfile.debian b/dockerfiles/Dockerfile.debian
index 9a0ceb3b..4e342a23 100644
--- a/dockerfiles/Dockerfile.debian
+++ b/dockerfiles/Dockerfile.debian
@@ -1,6 +1,8 @@
 # This should be run from the zef repo root
 FROM debian:stable
 
+WORKDIR /root/project
+
 RUN apt-get update
 RUN apt-get install -y --no-install-recommends \
     build-essential \
@@ -17,8 +19,9 @@ RUN apt-get install -y --no-install-recommends \
     libcurl4-openssl-dev \
     python3 \
     python3-pip \
-    python3-dev
+    python3-dev \
+    jq
 
 COPY . .
 
-RUN LIBZEF_LOCATION=$(realpath core) pip3 install ./python
\ No newline at end of file
+RUN LIBZEF_LOCATION=$(realpath core) pip3 install ./python
diff --git a/dockerfiles/Dockerfile.debian.localcompile b/dockerfiles/Dockerfile.debian.localcompile
new file mode 100644
index 00000000..353e7fb2
--- /dev/null
+++ b/dockerfiles/Dockerfile.debian.localcompile
@@ -0,0 +1,25 @@
+# This should be run from the zef repo root
+FROM debian:stable
+
+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 bash compile_for_local_dev.sh
diff --git a/dockerfiles/Dockerfile.ubuntu1804 b/dockerfiles/Dockerfile.ubuntu1804
new file mode 100644
index 00000000..3f0d9fd8
--- /dev/null
+++ b/dockerfiles/Dockerfile.ubuntu1804
@@ -0,0 +1,38 @@
+# This should be run from the zef repo root
+FROM ubuntu:18.04
+
+WORKDIR /root
+
+RUN apt update
+
+RUN apt 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 \
+    jq
+
+RUN apt install software-properties-common -y && \
+    add-apt-repository ppa:deadsnakes/ppa -y && \
+    apt update && \
+    apt install -y \
+        python3.8 \
+        python3.8-dev \
+        python3.8-distutils
+
+RUN curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py --silent && \
+    python3.8 get-pip.py
+
+WORKDIR /root/project
+
+COPY . .
+
+RUN LIBZEF_LOCATION=$(realpath core) python3.8 -mpip install ./python
diff --git a/python/pyproject.toml b/python/pyproject.toml
index 656fe7fc..cf1a9aca 100644
--- a/python/pyproject.toml
+++ b/python/pyproject.toml
@@ -2,7 +2,7 @@
 skip = ["pp*", "*musllinux*"]
 archs = ["auto64"]
 test-requires = "pytest networkx>=2.6.3"
-test-command = "bash {project}/scripts/assert_no_created_tokens.sh && ZEFHUB_AUTH_KEY=GUEST ZEFDB_QUIET=YES pytest {project}/python/tests"
+test-command = "bash {project}/scripts/assert_no_created_tokens.sh && ZEFHUB_AUTH_KEY=GUEST ZEFDB_QUIET=YES pytest {project}/python/tests && python {project}/python/tests/quick_start.py"
 
 [tool.cibuildwheel.linux]
 before-all = "yum install -y openssl-devel"
diff --git a/python/pyzef/CMakeLists.txt b/python/pyzef/CMakeLists.txt
index 3d858a10..18626d42 100644
--- a/python/pyzef/CMakeLists.txt
+++ b/python/pyzef/CMakeLists.txt
@@ -157,6 +157,34 @@ endif()
 # Anything else should not be installing
 install(TARGETS pyzef LIBRARY DESTINATION .)
 
+# We need the licenses for pybind11 and the pybind_json libraries. Technically,
+# as we could be compiling anywhere, we should grab the pybind11 license from
+# the site-packages directory... however it is not shipped in there! So instead
+# we will do our due diligence and manually include the license file.
+install(FILES
+  LICENSE.pybind11
+  LICENSE.pybind_json
+  DESTINATION .)
+
+# Finally we'll make a file up that references these license files and our own.
+# Note that the libzef license is going to be installed in the directory as
+# LICENSE.libzef and referencing its own dependencies.
+set(TOP_LICENSE_FILE "${CMAKE_CURRENT_BINARY_DIR}/LICENSE")
+install(FILES
+  ${TOP_LICENSE_FILE}
+  DESTINATION .)
+
+find_file(REPO_LICENSE
+  LICENSE
+  PATHS ${CMAKE_CURRENT_SOURCE_DIR}/../ ${CMAKE_CURRENT_SOURCE_DIR}/../../
+  NO_DEFAULT_PATH
+  NO_CACHE
+  REQUIRED)
+file(READ ${REPO_LICENSE} OUR_LICENSE)
+file(WRITE ${TOP_LICENSE_FILE} ${OUR_LICENSE})
+
+file(APPEND ${TOP_LICENSE_FILE} "\nThe following files contain additional bundled licenses:\n\nLICENSE.libzef\nLICENSE.pybind11\nLICENSE.pybind_json")
+
 #########################################
 # * All tests
 enable_testing ()
diff --git a/python/pyzef/CMakeLists2.txt b/python/pyzef/CMakeLists2.txt
deleted file mode 100644
index 8f245408..00000000
--- a/python/pyzef/CMakeLists2.txt
+++ /dev/null
@@ -1,43 +0,0 @@
-project(PyZefDB)
-
-get_filename_component(ZEFDB_DIR "../../" REALPATH)
-
-pybind11_add_module (pyzef
-  build_zefDB.cpp
-  zefops.cpp
-  internals.cpp
-  )
-
-set_target_properties(pyzef
-    PROPERTIES
-    CXX_STANDARD 17)
-
-target_include_directories (pyzef PUBLIC ${RANGE_INCLUDE_DIRS})
-
-if(UNIX AND NOT APPLE)
-  # This should take us up the lib directory where both libzef and libpython are.
-  set_target_properties(pyzef PROPERTIES INSTALL_RPATH "$ORIGIN")
-elseif(APPLE)
-  set_target_properties(pyzef PROPERTIES INSTALL_RPATH "@loader_path")
-endif()
-
-target_link_libraries(pyzef PUBLIC zef)
-
-add_custom_command(TARGET pyzef
-  POST_BUILD
-  COMMAND ${CMAKE_COMMAND} -E create_symlink $<TARGET_FILE:pyzef> ${ZEFDB_DIR}/zef/`basename $<TARGET_FILE:pyzef>`
-  COMMAND ${CMAKE_COMMAND} -E create_symlink $<TARGET_FILE:zef> ${ZEFDB_DIR}/zef/`basename $<TARGET_FILE:zef>`
-)
-  
-install ( TARGETS pyzef
-  LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}"
-  )
-
-# Things needed for Visual Studio
-if(MSVC)
-	target_compile_options(pyzef PUBLIC "/permissive-")
-    target_compile_options(pyzef PUBLIC "/bigobj")
-    set_target_properties(pyzef PROPERTIES SUFFIX ".pyd")
-    set_target_properties(pyzef PROPERTIES ENABLE_EXPORTS 1)
-	set_target_properties(pyzef PROPERTIES WINDOWS_EXPORT_ALL_SYMBOLS 1)
-endif()
diff --git a/python/pyzef/LICENSE.pybind11 b/python/pyzef/LICENSE.pybind11
new file mode 100644
index 00000000..c3fd6911
--- /dev/null
+++ b/python/pyzef/LICENSE.pybind11
@@ -0,0 +1,33 @@
+This library bundles the pybind11 library (https://github.com/pybind/pybind11).
+
+The full text of the pybind11 license follows:
+
+Copyright (c) 2016 Wenzel Jakob <wenzel.jakob@epfl.ch>, All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+Please also refer to the file .github/CONTRIBUTING.md, which clarifies licensing of
+external contributions to this project including patches, pull requests, etc.
diff --git a/python/pyzef/LICENSE.pybind_json b/python/pyzef/LICENSE.pybind_json
new file mode 100644
index 00000000..9452cba4
--- /dev/null
+++ b/python/pyzef/LICENSE.pybind_json
@@ -0,0 +1,33 @@
+This library bundles the pybind_json library (https://github.com/pybind/pybind11_json).
+
+The full text of the pybind11 license follows:
+
+BSD 3-Clause License
+
+Copyright (c) 2019,
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+* Redistributions of source code must retain the above copyright notice, this
+  list of conditions and the following disclaimer.
+
+* Redistributions in binary form must reproduce the above copyright notice,
+  this list of conditions and the following disclaimer in the documentation
+  and/or other materials provided with the distribution.
+
+* Neither the name of the copyright holder nor the names of its
+  contributors may be used to endorse or promote products derived from
+  this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/python/pyzef/build_zefDB.cpp b/python/pyzef/build_zefDB.cpp
index 9f0c0ef2..f5848976 100644
--- a/python/pyzef/build_zefDB.cpp
+++ b/python/pyzef/build_zefDB.cpp
@@ -165,32 +165,35 @@ PYBIND11_MODULE(pyzef, toplevel_module) {
 	py::class_<zefDB::EntityType>(main_module, "EntityType", py::buffer_protocol(), py::dynamic_attr())		
         .def(py::init<token_value_t>())
         .def_readonly("value", &EntityType::entity_type_indx)
-		.def("__repr_without_absorbed__", [](const EntityType& self)->std::string { return to_str(self); })
+		.def("__repr__", [](const EntityType& self)->std::string { return to_str(self); })
 		.def("__str__", [](const EntityType& self)->std::string { return str(self); })
 		.def("__eq__", [](const EntityType& self, const EntityType& other)->bool {return self==other; }, py::is_operator())
 		.def("__ne__", [](const EntityType& self, const EntityType& other)->bool {return self!=other; }, py::is_operator())
 		.def("__hash__", [](const EntityType& self) {return get_hash(self); })  // similar to the python hash for a python int: just use the int itself as the hash
 		.def("__int__", [](const EntityType& self)->int {return self.entity_type_indx; })  // similar to the python hash for a python int: just use the int itself as the hash
+		.def("__copy__", [](const EntityType& self)->EntityType {return self.entity_type_indx; })  // similar to the python hash for a python int: just use the int itself as the hash
 		;
 			
 	py::class_<zefDB::RelationType>(main_module, "RelationType", py::buffer_protocol(), py::dynamic_attr())
         .def(py::init<token_value_t>())
         .def_readonly("value", &RelationType::relation_type_indx)
-		.def("__repr_without_absorbed__", [](const RelationType& self)->std::string { return to_str(self); })
+		.def("__repr__", [](const RelationType& self)->std::string { return to_str(self); })
 		.def("__str__", [](const RelationType& self)->std::string {return str(self); })
 		.def("__eq__", [](const RelationType& self, const RelationType& other)->bool {return self.relation_type_indx == other.relation_type_indx; }, py::is_operator())
 		.def("__ne__", [](const RelationType& self, const RelationType& other)->bool {return self.relation_type_indx != other.relation_type_indx; }, py::is_operator())
 		.def("__hash__", [](const RelationType& self) {return get_hash(self); })
+		.def("__copy__", [](const RelationType& self)->RelationType {return self; })
 		;
 
 	py::class_<zefDB::Keyword>(main_module, "Keyword", py::buffer_protocol(), py::dynamic_attr())
         .def(py::init<token_value_t>())
         .def_readonly("value", &Keyword::indx)
-		.def("__repr_without_absorbed__", [](const Keyword& self)->std::string { return to_str(self); })
+		.def("__repr__", [](const Keyword& self)->std::string { return to_str(self); })
 		.def("__str__", [](const Keyword& self)->std::string {return str(self); })
 		.def("__eq__", [](const Keyword& self, const Keyword& other)->bool {return self.indx == other.indx; }, py::is_operator())
 		.def("__ne__", [](const Keyword& self, const Keyword& other)->bool {return self.indx != other.indx; }, py::is_operator())
 		.def("__hash__", [](const Keyword& self) {return get_hash(self); })
+		.def("__copy__", [](const Keyword& self)->Keyword {return self; })
 		;
 
 
diff --git a/python/pyzef/external/external.cmake b/python/pyzef/external/external.cmake
index ae9795cb..bdddc8b6 100644
--- a/python/pyzef/external/external.cmake
+++ b/python/pyzef/external/external.cmake
@@ -44,7 +44,7 @@ endfunction()
 # # set(nlohmann_json_DIR ${nlohmann_json_SOURCE_DIR} CACHE PATH "" FORCE)
 
 # * pybind11 json
-message(STATUS "External: pybind11")
+message(STATUS "External: pybind11_json")
 
 FetchContent_Declare(pybind11_json
   GIT_REPOSITORY https://github.com/pybind/pybind11_json
diff --git a/python/pyzef/internals.cpp b/python/pyzef/internals.cpp
index 7587e3b8..914a0e41 100644
--- a/python/pyzef/internals.cpp
+++ b/python/pyzef/internals.cpp
@@ -231,12 +231,13 @@ void fill_internals_module(py::module_ & internals_submodule) {
 	py::class_<AtomicEntityType>(internals_submodule, "AtomicEntityType", py::dynamic_attr())
 		.def(py::init<enum_indx>())	
 		.def_readonly("value", &AtomicEntityType::value)
-		.def("__repr_without_absorbed__", [](const AtomicEntityType& self)->std::string { return to_str(self); })
+		.def("__repr__", [](const AtomicEntityType& self)->std::string { return to_str(self); })
 		.def("__str__", [](const AtomicEntityType& self)->std::string { return str(self); })
 		.def("__eq__", [](const AtomicEntityType& self, const AtomicEntityType& other)->bool { return self == other; }, py::is_operator())
 		.def("__ne__", [](const AtomicEntityType& self, const AtomicEntityType& other)->bool { return self != other; }, py::is_operator())
 		.def("__hash__", [](const AtomicEntityType& self)->enum_indx {return self.value; })
 		.def("__int__", [](const AtomicEntityType& self)->int {return self.value; })
+		.def("__copy__", [](const AtomicEntityType& self)->AtomicEntityType {return self; })
 
 		.def_property_readonly("__enum_type", [](const AtomicEntityType& self) { return internals::get_enum_type_from_aet(self); })
 		.def_property_readonly("__unit", [](const AtomicEntityType& self) { return internals::get_unit_from_aet(self); })
@@ -407,7 +408,7 @@ void fill_internals_module(py::module_ & internals_submodule) {
 		.def("__eq__", &DelegateRelationTriple::operator==, py::is_operator())
         ;
 
-    py::class_<Delegate>(internals_submodule, "Delegate", py::buffer_protocol(), py::call_guard<py::gil_scoped_release>())
+    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>())
@@ -422,6 +423,7 @@ void fill_internals_module(py::module_ & internals_submodule) {
 		.def_readonly("item", &Delegate::item)
         .def("__repr__", [](const Delegate & d) { return to_str(d); })
 		.def("__eq__", &Delegate::operator==, py::is_operator())
+		.def("__copy__", [](const Delegate & d)->Delegate { return d; })
         ;
 
 
@@ -743,4 +745,7 @@ void fill_internals_module(py::module_ & internals_submodule) {
         return Butler::get_butler()->get_local_process_graph();
     },
         py::call_guard<py::gil_scoped_release>());
+
+    internals_submodule.def("register_merge_handler", &Butler::register_merge_handler);
+    internals_submodule.add_object("_cleanup_merge_handler", py::capsule(&Butler::remove_merge_handler));
 }
diff --git a/python/setup.py b/python/setup.py
index 601a6a5c..5deb7f29 100644
--- a/python/setup.py
+++ b/python/setup.py
@@ -93,8 +93,13 @@ def build_extensions(self):
 
 class override_sdist(sdist):
     def make_release_tree(self, base_dir, files):
+        import shutil
+
         sdist.make_release_tree(self, base_dir, files)
 
+        print("* Adding license to sdist")
+        shutil.copy("../LICENSE", os.path.join(base_dir, "LICENSE"))
+
         print("* Adding libzef to sdist")
 
         # Adding in the libzef core too, using git to obtain the list of files.
@@ -109,8 +114,10 @@ def make_release_tree(self, base_dir, files):
 
         print("** Adding in the get_zeftypes.py script manually")
         # We need to include the get_zeftypes.py file along with the distribution
-        import shutil
-        shutil.copy("../scripts/get_zeftypes.py", os.path.join(base_dir, "libzef", "scripts"))
+        shutil.copy("../scripts/get_zeftypes.py", os.path.join(target_dir, "scripts"))
+
+        print("** Adding in the LICENSE file")
+        shutil.copy("../LICENSE", target_dir)
 
         print("** Updating the setup.cfg file")
         # We now overwrite the setup.cfg file to point at this new directory
diff --git a/python/zef/__init__.py b/python/zef/__init__.py
index 212386b4..19c1dd0a 100644
--- a/python/zef/__init__.py
+++ b/python/zef/__init__.py
@@ -15,25 +15,6 @@
 from . import _version
 __version__ = _version.get_versions()['version']
 
-import os
-if os.environ.get("ZEFDB_DEVELOPER_CIRCULAR_IMPORTS", "FALSE") == "TRUE":
-    from .circular_imports import check_circular_imports
-    check_circular_imports()
-
-# See comments in libzef CMakeLists.txt. This is to pass appropriate certificate
-# information to libzef if it is bundled with pyzef.
-try:
-    # I don't think this will ever fail as python requires an SSL to build now I
-    # think. What this block should really be doing is checking for OpenSSL not
-    # LibreSSL... or maybe the libzef library should be using LibreSSL on macos.
-    import ssl
-except ImportError:
-    raise ImportError("zef requires OpenSSL to be installed.")
-
-_ssl_paths = ssl.get_default_verify_paths()
-os.environ["LIBZEF_CA_BUNDLE"] = _ssl_paths.cafile or ""
-os.environ["LIBZEF_CA_PATH"] = _ssl_paths.capath or ""
-
 ########################################################
 # * Exposing common functions
 #------------------------------------------------------
diff --git a/python/zef/core/VT/__init__.py b/python/zef/core/VT/__init__.py
index 54f08745..ac9c9a39 100644
--- a/python/zef/core/VT/__init__.py
+++ b/python/zef/core/VT/__init__.py
@@ -28,64 +28,88 @@ def time_ctor(*args, **kwargs):
     return pyzef.main.Time(*args, **kwargs)
 
 
+def bytes_ctor(*args, **kwargs):
+    # from ... import core
+    from ...core.bytes import Bytes_
+    # return core.bytes.Bytes_(*args, **kwargs)
+    return Bytes_(*args, **kwargs)
+
+
+def decimal_ctor(*args, **kwargs):
+    # from ... import core
+    from ...core.decimal import Decimal_
+    # 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)
 
 
-Error.RuntimeError('abcd')
-
-Nil        = ValueType(type_name='Nil',        constructor_func=None)
-Any        = ValueType(type_name='Any',        constructor_func=None)
-Bool       = ValueType(type_name='Bool',       constructor_func=bool)
-Int        = ValueType(type_name='Int',        constructor_func=int)
-Float      = ValueType(type_name='Float',      constructor_func=float)
-String     = ValueType(type_name='String',     constructor_func=str)
-List       = ValueType(type_name='List',       constructor_func=tuple)
-Dict       = ValueType(type_name='Dict',       constructor_func=dict)
-Set        = ValueType(type_name='Set',        constructor_func=set)
-EZefRef    = ValueType(type_name='EZefRef',    constructor_func=None)    
-ZefRef     = ValueType(type_name='ZefRef',     constructor_func=None)     
-Graph      = ValueType(type_name='Graph',      constructor_func=graph_ctor)     
-GraphSlice = ValueType(type_name='GraphSlice', constructor_func=None)     
-FlatGraph  = ValueType(type_name='FlatGraph',  constructor_func=None)     
-ZefOp      = ValueType(type_name='ZefOp',      constructor_func=None)     
-Stream     = ValueType(type_name='Stream',     constructor_func=None)     
-TX         = ValueType(type_name='TX',         constructor_func=None)     
-Time       = ValueType(type_name='Time',       constructor_func=time_ctor)
-Error      = ValueType(type_name='Error',      constructor_func=None)
-Image      = ValueType(type_name='Image',      constructor_func=None)
-
-UID        = ValueType(type_name='UID',        constructor_func=None)       
-BaseUID    = ValueType(type_name='BaseUID',    constructor_func=None)       
-ZefRefUID  = ValueType(type_name='ZefRefUID',  constructor_func=None)        
-EternalUID = ValueType(type_name='EternalUID', constructor_func=None)       
-
-LazyValue  = ValueType(type_name='LazyValue',  constructor_func=None) 
-Awaitable  = ValueType(type_name='Awaitable',  constructor_func=None) 
-
-
-
-AET        = ValueType(type_name='AET',        constructor_func=None)
-ET         = ValueType(type_name='ET',         constructor_func=None)
-RT         = ValueType(type_name='RT',         constructor_func=None)
-BT         = ValueType(type_name='BT',         constructor_func=None)
-Enum       = ValueType(type_name='Enum',       constructor_func=None)
-Record     = ValueType(type_name='Record',     constructor_func=None)       
-Function   = ValueType(type_name='Function',   constructor_func=None)
-GraphDelta = ValueType(type_name='GraphDelta', constructor_func=None)     
-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)     
-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)     
-
-
-
-# These are special classes: using them with [...] returns a ValueType though
+
+Nil        = ValueType_(type_name='Nil',        constructor_func=None)
+Any        = ValueType_(type_name='Any',        constructor_func=None)
+Bool       = ValueType_(type_name='Bool',       constructor_func=bool)
+Int        = ValueType_(type_name='Int',        constructor_func=int)
+Float      = ValueType_(type_name='Float',      constructor_func=float)
+String     = ValueType_(type_name='String',     constructor_func=str)
+Bytes      = ValueType_(type_name='Bytes',      constructor_func=bytes_ctor)
+Decimal    = ValueType_(type_name='Decimal',    constructor_func=decimal_ctor)
+List       = ValueType_(type_name='List',       constructor_func=tuple)
+Dict       = ValueType_(type_name='Dict',       constructor_func=dict)
+Set        = ValueType_(type_name='Set',        constructor_func=set)
+EZefRef    = ValueType_(type_name='EZefRef',    constructor_func=None)    
+ZefRef     = ValueType_(type_name='ZefRef',     constructor_func=None)     
+Graph      = ValueType_(type_name='Graph',      constructor_func=graph_ctor)     
+GraphSlice = ValueType_(type_name='GraphSlice', constructor_func=None)     
+FlatGraph  = ValueType_(type_name='FlatGraph',  constructor_func=None)     
+ZefOp      = ValueType_(type_name='ZefOp',      constructor_func=None)     
+Stream     = ValueType_(type_name='Stream',     constructor_func=None)     
+TX         = ValueType_(type_name='TX',         constructor_func=None)     
+Time       = ValueType_(type_name='Time',       constructor_func=time_ctor)
+Error      = ValueType_(type_name='Error',      constructor_func=None)
+Image      = ValueType_(type_name='Image',      constructor_func=None)
+ValueType  = ValueType_(type_name='ValueType',  constructor_func=ValueType_)
+
+
+# QuantityInt= ValueType_(type_name='QuantityInt',constructor_func=None)
+# QuantityFloat= ValueType_(type_name='QuantityFloat',constructor_func=None)
+
+UID        = ValueType_(type_name='UID',        constructor_func=None)       
+BaseUID    = ValueType_(type_name='BaseUID',    constructor_func=None)       
+ZefRefUID  = ValueType_(type_name='ZefRefUID',  constructor_func=None)        
+EternalUID = ValueType_(type_name='EternalUID', constructor_func=None)       
+
+Instantiated = ValueType_(type_name='Instantiated', constructor_func=None)       
+Terminated = ValueType_(type_name='Terminated', constructor_func=None)       
+Assigned   = ValueType_(type_name='Assigned',   constructor_func=None)       
+Tagged     = ValueType_(type_name='Tagged',     constructor_func=None)       
+
+LazyValue  = ValueType_(type_name='LazyValue',  constructor_func=None) 
+Awaitable  = ValueType_(type_name='Awaitable',  constructor_func=None) 
+
+
+
+AET        = ValueType_(type_name='AET',        constructor_func=None)
+ET         = ValueType_(type_name='ET',         constructor_func=None)
+RT         = ValueType_(type_name='RT',         constructor_func=None)
+BT         = ValueType_(type_name='BT',         constructor_func=None)
+Enum       = ValueType_(type_name='Enum',       constructor_func=None)
+Record     = ValueType_(type_name='Record',     constructor_func=None)       
+Function   = ValueType_(type_name='Function',   constructor_func=None)
+GraphDelta = ValueType_(type_name='GraphDelta', constructor_func=None)     
+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)     
+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)     
+
+
+
+# These are special classes: using them with [...] returns a ValueType_ though
 Union = UnionClass()
 Intersection = IntersectionClass()
 SetOf = SetOfClass()
diff --git a/python/zef/core/VT/value_type.py b/python/zef/core/VT/value_type.py
index 123f611a..a5f3bc82 100644
--- a/python/zef/core/VT/value_type.py
+++ b/python/zef/core/VT/value_type.py
@@ -13,7 +13,7 @@
 # limitations under the License.
 
 """
-In Zef the types of Zef Values are values themselves: ValueType.
+In Zef the types of Zef Values are values themselves: ValueType_.
 These are distinct from the host language's implementation type
 of the value instance.
 
@@ -57,7 +57,7 @@
 Union
 SetOf
 
-There is also a Union ValueType, which is returned.
+There is also a Union ValueType_, which is returned.
 This custom structure may rearrange and parse terms.
 
 
@@ -69,7 +69,7 @@
 # This looks up the associated function 
 _value_type_constructor_funcs = {}
 
-class ValueType:
+class ValueType_:
     """ 
     Zef ValueTypes are Values themselves.
     """
@@ -98,11 +98,11 @@ def __call__(self, *args, **kwargs):
 
 
     def __getitem__(self, x):
-        return ValueType(self.d["type_name"], absorbed=(*self.d['absorbed'], x))
+        return ValueType_(self.d["type_name"], absorbed=(*self.d['absorbed'], x))
 
 
     def __eq__(self, other):
-        if not isinstance(other, ValueType): return False
+        if not isinstance(other, ValueType_): return False
         return self.d['type_name'] == other.d['type_name'] and self.d['absorbed'] == other.d['absorbed']
 
 
@@ -112,18 +112,18 @@ def __hash__(self):
 
     def __or__(self, other):
         from ..op_structs import ZefOp
-        if isinstance(other, ValueType):
-            return simplify_value_type(ValueType(type_name='Union', absorbed=(self, other,)))
+        if isinstance(other, ValueType_):
+            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)}')
+            raise Exception(f'"ValueType_`s "|" called with unsupported type {type(other)}')
     
     def __and__(self, other):
-        if isinstance(other, ValueType):
-            return simplify_value_type(ValueType(type_name='Intersection', absorbed=(self, other,)))
+        if isinstance(other, ValueType_):
+            return simplify_value_type(ValueType_(type_name='Intersection', absorbed=(self, other,)))
         else:
-            raise Exception(f'"ValueType`s "&" called with unsupported type {type(other)}')
+            raise Exception(f'"ValueType_`s "&" called with unsupported type {type(other)}')
     
 
 
@@ -132,11 +132,11 @@ 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,))
+            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,))
+            return ValueType_(type_name='Union', absorbed=(x,))
         else:
             raise Exception(f'"Union[...]" called with unsupported type {type(x)}')
             
@@ -145,11 +145,11 @@ 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,))
+            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,))
+            return ValueType_(type_name='Intersection', absorbed=(x,))
         else:
             raise Exception(f'"Intersection[...]" called with unsupported type {type(x)}')
             
@@ -159,11 +159,11 @@ class SetOfClass:
     def __getitem__(self, x):
         from ..op_structs import ZefOp
         if isinstance(x, tuple):
-            return ValueType(type_name='SetOf', absorbed=x)
-        elif isinstance(x, ValueType):
-            return ValueType(type_name='SetOf', absorbed=(x,))
+            return ValueType_(type_name='SetOf', absorbed=x)
+        elif isinstance(x, ValueType_):
+            return ValueType_(type_name='SetOf', absorbed=(x,))
         elif isinstance(x, ZefOp):
-            return ValueType(type_name='SetOf', absorbed=(x,))
+            return ValueType_(type_name='SetOf', absorbed=(x,))
         else:
             raise Exception(f'"SetOf[...]" called with unsupported type {type(x)}')
             
@@ -215,11 +215,11 @@ def is_a_intersection(y):
         # flatten out unions: Union[Union[A][B]][C]  == Union[A][B][C]
         old_abs = x.d['absorbed']
         absorbed = tuple((el.d['absorbed'] if is_a_union(el) else (el,) for el in old_abs))  # flatten this out
-        return ValueType(type_name='Union', absorbed=tuple(make_distinct((a2 for a1 in absorbed for a2 in a1))))
+        return ValueType_(type_name='Union', absorbed=tuple(make_distinct((a2 for a1 in absorbed for a2 in a1))))
     elif is_a_intersection(x):
         # flatten out Intersections: Intersection[Intersection[A][B]][C]  == Intersection[A][B][C]
         old_abs = x.d['absorbed']
         absorbed = tuple((el.d['absorbed'] if is_a_intersection(el) else (el,) for el in old_abs))  # flatten this out
-        return ValueType(type_name='Intersection', absorbed=tuple(make_distinct((a2 for a1 in absorbed for a2 in a1))))
+        return ValueType_(type_name='Intersection', absorbed=tuple(make_distinct((a2 for a1 in absorbed for a2 in a1))))
     else:
         return x
diff --git a/python/zef/core/__init__.py b/python/zef/core/__init__.py
index af5533aa..5e15bccc 100644
--- a/python/zef/core/__init__.py
+++ b/python/zef/core/__init__.py
@@ -28,10 +28,27 @@
 # except:
 #     print(f"FAILED TO FIND LIBZEF at {path} - hopefully pyzef has it hard-coded (true when running from source directory)")
 
+# ** Circular import checks
+import os
 if os.environ.get("ZEFDB_DEVELOPER_CIRCULAR_IMPORTS", "FALSE") == "TRUE":
     from .circular_imports import check_circular_imports
     check_circular_imports()
 
+# ** TLS certificates
+# See comments in libzef CMakeLists.txt. This is to pass appropriate certificate
+# information to libzef if it is bundled with pyzef.
+try:
+    # I don't think this will ever fail as python requires an SSL to build now I
+    # think. What this block should really be doing is checking for OpenSSL not
+    # LibreSSL... or maybe the libzef library should be using LibreSSL on macos.
+    import ssl
+except ImportError:
+    raise ImportError("zef requires OpenSSL to be installed.")
+
+_ssl_paths = ssl.get_default_verify_paths()
+os.environ["LIBZEF_CA_BUNDLE"] = _ssl_paths.cafile or ""
+os.environ["LIBZEF_CA_PATH"] = _ssl_paths.capath or ""
+
 ########################################################
 # * Exposing common functions
 #------------------------------------------------------
@@ -72,7 +89,7 @@
 from .flat_graph import FlatGraph, FlatRef, FlatRefs, Val
 
 from .VT import TX
-from .abstract_raes import Entity, AtomicEntity, Relation
+from .abstract_raes import Entity, AtomicEntity, Relation, TXNode, Root
 
 from .zef_functions import func
 
@@ -85,7 +102,7 @@
 from . import op_implementations
 
 from .abstract_raes import make_custom_entity
-from .VT.value_type import ValueType
+from .VT.value_type import ValueType_
 
 # instantiating these here, since not all of the core has been
 # initialized when Python imports the abstract_raes module
@@ -95,6 +112,29 @@
 please_assign      = make_custom_entity(name_to_display='please_assign',    predetermined_uid='4d4a93522f75ed21')
 
 
+# TODO: import the other ValueTypes here and implement constructor by forwarding args
+from .VT import (
+    Nil,
+    Any,
+    Bool,
+    Int,
+    Float,
+    String,
+    Bytes,
+    Decimal,
+    List,
+    Dict,
+    Set,
+    ValueType,
+    Instantiated, 
+    Terminated, 
+    Assigned,
+
+    Union,
+    Intersection,
+    SetOf,
+    )
+
 pyzef.internals.finished_loading_python_core()
 
 # ############################################
diff --git a/python/zef/core/_ops.py b/python/zef/core/_ops.py
index 39896064..de6a7baf 100644
--- a/python/zef/core/_ops.py
+++ b/python/zef/core/_ops.py
@@ -187,6 +187,7 @@ def make_zefop(rt):
 is_represented_as = make_zefop(RT.IsRepresentedAs)
 zef_type        = make_zefop(RT.ZefType)
 rae_type        = make_zefop(RT.RaeType)
+abstract_type   = make_zefop(RT.AbstractType)
 fill_or_attach  = make_zefop(RT.FillOrAttach)
 Assert          = make_zefop(RT.Assert)
 allow_tombstone = make_zefop(RT.AllowTombstone)
diff --git a/python/zef/core/abstract_raes.py b/python/zef/core/abstract_raes.py
index c8232b65..26710256 100644
--- a/python/zef/core/abstract_raes.py
+++ b/python/zef/core/abstract_raes.py
@@ -203,6 +203,86 @@ def __getitem__(self, x):
         # return merged[self][internal_id]
         return Relation({**self.d, 'absorbed': (*self.d['absorbed'], x)})
 
+class TXNode:
+    """ 
+    A value representation of an "abstract transaction".
+    """
+    def __init__(self, x):
+        from ._ops import origin_uid
+        if isinstance(x, ZefRef) or isinstance(x, EZefRef):
+            assert BT(x)==BT.TX_EVENT_NODE
+            self.d = {
+                'uid': origin_uid(x),
+                'absorbed': (),
+            }
+        
+        elif isinstance(x, TXNode):
+            self.d = {
+                'uid': x.d['uid'],
+                'absorbed': x.d['absorbed']
+            }
+
+        else:
+            raise TypeError(f"can't construct an abstract transaction from a {type(x)=}.  Value passed in: {x=}")
+
+    def __repr__(self):
+        # also show any absorbed elements
+        base_name = f'TXNode({repr(self.d["uid"])})' 
+        return base_name + ''.join(('[' + repr(el) + ']' for el in self.d['absorbed']))
+
+    def __eq__(self, other):
+        if not isinstance(other, TXNode): return False
+        return self.d['uid'] == other.d['uid'] and self.d['absorbed'] == other.d['absorbed']
+
+    def __hash__(self):
+        return hash((TXNode, self.d['uid']))
+
+    def __getitem__(self, x):
+        # append x to absorbed (a tuple). Return a new object
+        temp = TXNode(self)
+        temp.d['absorbed'] = (*self.d['absorbed'], x)
+        return temp
+        
+
+class Root:
+    """ 
+    A value representation of an "abstract root node".
+    """
+    def __init__(self, x):
+        from ._ops import origin_uid
+        if isinstance(x, ZefRef) or isinstance(x, EZefRef):
+            assert BT(x)==BT.ROOT_NODE
+            self.d = {
+                'uid': origin_uid(x),
+                'absorbed': (),
+            }
+        
+        elif isinstance(x, Root):
+            self.d = {
+                'uid': x.d['uid'],
+                'absorbed': x.d['absorbed']
+            }
+
+        else:
+            raise TypeError(f"can't construct an abstract root node from a {type(x)=}.  Value passed in: {x=}")
+
+    def __repr__(self):
+        # also show any absorbed elements
+        base_name = f'Root({repr(self.d["uid"])})' 
+        return base_name + ''.join(('[' + repr(el) + ']' for el in self.d['absorbed']))
+
+    def __eq__(self, other):
+        if not isinstance(other, Root): return False
+        return self.d['uid'] == other.d['uid'] and self.d['absorbed'] == other.d['absorbed']
+
+    def __hash__(self):
+        return hash((Root, self.d['uid']))
+
+    def __getitem__(self, x):
+        # append x to absorbed (a tuple). Return a new object
+        temp = Root(self)
+        temp.d['absorbed'] = (*self.d['absorbed'], x)
+        return temp
 
 
 def abstract_rae_from_rae_type_and_uid(rae_type, uid):
diff --git a/python/zef/core/bytes.py b/python/zef/core/bytes.py
index 04c2ce73..fb0e3c26 100644
--- a/python/zef/core/bytes.py
+++ b/python/zef/core/bytes.py
@@ -12,22 +12,21 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-class Bytes:
-    def __init__(self, data):
+class Bytes_:
+    """
+    The default repr uses hexadecimal encoding.
+    """
+    def __init__(self, x):
         import string
-        if isinstance(data, bytes):
-            self.data = data
-            self.is_hex = False
-        elif isinstance(data, str) and all(c in string.hexdigits for c in data):
-            self.data = data
-            self.is_hex = True
+        if isinstance(x, bytes):
+            self.data = x
+        elif isinstance(x, str) and all(c in string.hexdigits for c in x):
+            self.data = x.encode()    # use utf8
         else:
-            raise NotImplementedError
+            raise NotImplementedError(f"constructing a bytes from {x=}")
     
     def __repr__(self):
-        if self.is_hex: return f'Bytes("{self.data})"'
-        else: return f'Bytes("{self.data.hex()}")'
+        return f'Bytes("{self.data.hex()}")'
 
     def __str__(self):
-        if self.is_hex: return self.data
-        else: return self.data.hex()
\ No newline at end of file
+        return self.data.hex()
diff --git a/python/zef/core/decimal.py b/python/zef/core/decimal.py
new file mode 100644
index 00000000..72e96413
--- /dev/null
+++ b/python/zef/core/decimal.py
@@ -0,0 +1,102 @@
+# 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.
+
+
+class Decimal_:
+    def __init__(self, val, decimal_positions):
+        if not isinstance(decimal_positions, int):
+            raise ValueError(f"in constructing a decimal: decimal_positions must be an int. got {type(decimal_positions)}")
+        
+        if isinstance(val, float):
+            self.val_int_encoded = round(val * 10**decimal_positions)
+            self.decimal_positions = decimal_positions
+        elif isinstance(val, str):
+            pos = val.find('.')
+            pre = val if pos==-1 else val[:pos]
+            post = (f"{{:0<{decimal_positions}}}").format('' if pos==-1 else val[pos+1:pos+decimal_positions+1])
+            # check whether we need to round up
+            m = decimal_positions+2
+            postpost = val[m:m+1]
+            rounding_term = 1 if len(postpost)==1 and int(postpost)>4 else 0
+            self.val_int_encoded = int(pre+post) + rounding_term
+            self.decimal_positions = decimal_positions
+        else:
+            raise ValueError(f"Can't construct a Decimal from a {type(val)}")
+
+    def __repr__(self):
+        # correct for repr output if decimal starts with '0': pad str repr of int with zeros explicitly        
+        s=(f"{{:0>{self.decimal_positions}}}").format(str(self.val_int_encoded))
+        return f"Decimal({s[:-self.decimal_positions]}.{s[-self.decimal_positions:]}, {self.decimal_positions})"
+
+    def __str__(self):
+        s=(f"{{:0>{self.decimal_positions}}}").format(str(self.val_int_encoded))
+        return f"{s[:-self.decimal_positions]}.{s[-self.decimal_positions:]}"
+
+    def __float__(self):
+        return float(self.val_int_encoded / 10**self.decimal_positions)
+    
+    def __add__(self, other):
+        if isinstance(other, int):
+            return Decimal_((self.val_int_encoded+ other * 10**self.decimal_positions)/10**self.decimal_positions , self.decimal_positions)
+        elif isinstance(other, float):
+            return Decimal_((self.val_int_encoded + round(other * 10**self.decimal_positions)) /10**self.decimal_positions, self.decimal_positions)
+        elif isinstance(other, Decimal_):
+            """
+            Use the higher decimal precision of the two operands for the output
+            """
+            # pos_max = max(self.decimal_positions, other.decimal_positions)
+            # pos_min = min(self.decimal_positions, other.decimal_positions)
+            
+            # return Decimal_((self.val_int_encoded + round(other.val_int_encoded * 10**self.decimal_positions)) 
+            #  /10**pos_max, pos_max)
+            raise NotImplementedError
+        else:
+            raise TypeError
+    
+    def __radd__(self, other):
+        return self+other
+    
+    def __sub__(self, other):
+        return self+ (-1*other)
+    
+    def __rsub__(self, other):
+        # :(  very mutaty to avoid unnecessary multiplications and divisions
+        tmp = Decimal_(0.0, self.decimal_positions)
+        tmp.val_int_encoded = -self.val_int_encoded
+        return tmp + other
+
+    def __neg__(self):
+        """
+        allow writing "-my_decimal"
+        """
+        tmp = Decimal_(0.0, self.decimal_positions)
+        tmp.val_int_encoded = -self.val_int_encoded
+        return tmp
+
+    def __pos__(self):
+        """
+        allow writing "+my_decimal"
+        """
+        return self
+    
+    def __mul__(self, x):
+        if not isinstance(x, int):
+            raise NotImplementedError('You can only multiply Decimal with integers for now')
+        tmp = Decimal_(0.0, self.decimal_positions)
+        tmp.val_int_encoded = x*self.val_int_encoded
+        return tmp
+
+    def __rmul__(self, x):
+        return self*x
+
diff --git a/python/zef/core/fx/http.py b/python/zef/core/fx/http.py
index dfbaacde..766ae168 100644
--- a/python/zef/core/fx/http.py
+++ b/python/zef/core/fx/http.py
@@ -80,8 +80,8 @@ def handle_generic(self, method):
                 pass
 
             status,headers,msg = result
-
             if status != 200:
+                msg = msg.decode('utf-8')
                 self.send_response(status, msg)
             else:
                 self.send_response(status)
@@ -90,6 +90,9 @@ def handle_generic(self, method):
             self.end_headers()
             if status == 200:
                 self.wfile.write(msg)
+        except BrokenPipeError:
+            log.error("Connection aborted unexpectedly")
+            pass
         except Exception as exc:
             # TODO: Place this error into a stream available to the user
             log.error("Some problem in handling HTTP request", exc_info=exc)
@@ -215,7 +218,7 @@ def http_send_response_handler(eff: Effect):
     headers = copy.deepcopy(headers)
 
     header_names = set(k.lower() for k in headers)
-    if msg is not None:
+    if msg is not None and status == 200:
         if "content-length" not in header_names:
             headers["Content-Length"] = str(len(msg))
         if "content-type" not in header_names:
diff --git a/python/zef/core/graph_delta.py b/python/zef/core/graph_delta.py
index e2878535..0fc5b3e0 100644
--- a/python/zef/core/graph_delta.py
+++ b/python/zef/core/graph_delta.py
@@ -25,7 +25,7 @@
 from ._ops import *
 from .op_structs import ZefOp, LazyValue
 from .graph_slice import GraphSlice
-from .abstract_raes import Entity, Relation, AtomicEntity
+from .abstract_raes import Entity, Relation, AtomicEntity, TXNode, Root
 
 from abc import ABC
 class ListOrTuple(ABC):
@@ -233,24 +233,6 @@ def obtain_ids(x) -> dict:
     ids = {}
     if isinstance(x, LazyValue):
         ids = obtain_ids(x.initial_val)
-    elif isinstance(x, ZefOp):
-        if is_a(x, instantiated):
-            # is there an internal id curried in?
-            if len(x.el_ops[0][1]) > 1:
-                internal_id = get_curried_arg(x, 1)
-                rae_type = get_curried_arg(x, 0)
-                ids = {internal_id: rae_type}
-
-        elif is_a(x, merged):
-            # is there an internal id curried in?
-            if len(x.el_ops[0][1]) > 1:
-                internal_id = get_curried_arg(x, 1)
-                obj = get_curried_arg(x, 0)
-                ids = {internal_id: obj}
-
-        elif is_a(x, assign_value):
-            ids = obtain_ids(get_curried_arg(x, 0))
-
     elif is_a(x, Delegate):
         ids = {x: x}
 
@@ -267,7 +249,7 @@ def obtain_ids(x) -> dict:
                 ids = merge_no_overwrite(ids, obtain_ids(target(x)))
 
     elif type(x) in [ZefRef, EZefRef]:
-        ids = {uid(to_ezefref(x)): x}
+        ids = {origin_uid(x): x}
 
 
     # This is an extra step on top of the previous checks
@@ -293,7 +275,9 @@ def verify_input_el(x, id_definitions, allow_rt=False, allow_scalar=False):
             return
         elif is_a(x.el_ops, terminate):
             return
-        raise Exception(f"A LazyValue must have come from (x <= value) or (x | terminate) only. Got {x}")
+        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}")
 
     # Note: just is_a(x, Z) will also mean ZefRefs will be hit
     elif is_a(x, ZefOp) and is_a(x, Z):
@@ -331,21 +315,13 @@ def verify_input_el(x, id_definitions, allow_rt=False, allow_scalar=False):
         return
 
     elif isinstance(x, ZefOp):
-        if length(LazyValue(x) | absorbed) > 1:
-            # This could be something like AET.Int["asdf"] <= 5
-            if x | peel | last | is_a[assign_value]:
+        if len(x) > 1:
+            # This could be something like Z["asdf"] | assign_value[5]
+            if x | peel | first | is_a[Z]:
                 return
         if length(LazyValue(x) | absorbed) != 1:
             raise Exception(f"ZefOp has more than one op inside of it. {x}")
-        if is_a(x, terminate):
-            return
-        if is_a(x, merged):
-            return
-        if is_a(x, assign_value):
-            return
-        if is_a(x, tag):
-            return
-        raise Exception(f"Not allowing ZefOps except for terminate, assign_value, delegate and tag. Got {x}")
+        raise Exception(f"Not allowing ZefOps except for those starting with Z. Got {x}")
 
     elif is_a(x, Delegate):
         return
@@ -425,8 +401,6 @@ def iteration_step(state: dict, gen_id)->dict:
             'commands': cmds,
     }
 
-
-
 def dispatch_cmds_for(expr, gen_id):
     if isinstance(expr, LazyValue):
         expr = collect(expr)
@@ -437,22 +411,15 @@ def dispatch_cmds_for(expr, gen_id):
             return cmds_for_lv_assign_value(expr, gen_id)
         elif is_a(expr.el_ops, terminate):
             return cmds_for_lv_terminate(expr)
+        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")
 
     elif isinstance(expr, ZefOp):
-        if False:
-            pass
-        # elif is_a(expr, terminate):
-        #     return cmds_for_terminate(expr)
-        # elif is_a(expr, assign_value):
-        #     return cmds_for_assign_value(expr)
-        elif is_a(expr, instantiated):
-            return cmds_for_instantiated(expr)
-        elif is_a(expr, merged):
-            return cmds_for_merged(expr)
-        elif is_a(expr, tag):
-            return cmds_for_tag(expr)
+        # If we have a chain beginning with a Z, convert to LazyValue
+        if len(expr) > 1 and is_a(LazyValue(expr) | peel | first | collect, Z):
+            return cmds_for_initial_Z(expr)
 
         raise RuntimeError(f'We should not have landed here, with expr={expr}')
     if is_a(expr, Delegate):
@@ -467,6 +434,8 @@ def dispatch_cmds_for(expr, gen_id):
         Entity: cmds_for_mergable,
         AtomicEntity: cmds_for_mergable,
         Relation: cmds_for_mergable,
+        TXNode: cmds_for_mergable,
+        Root: cmds_for_mergable,
 
         tuple: P(cmds_for_tuple, gen_id=gen_id),
         list: P(cmds_for_tuple, gen_id=gen_id),
@@ -487,6 +456,13 @@ def dispatch_cmds_for(expr, gen_id):
 #     iid,exprs = realise_single_node(x, gen_id)
 #     return exprs, [], ()
 
+def cmds_for_initial_Z(expr):
+    assert len(expr) > 1 and LazyValue(expr) | peel | first | is_a[Z] | collect
+
+    expr = LazyValue(LazyValue(expr) | peel | first | collect) | (LazyValue(expr) | peel | skip[1] | as_pipeline | collect)
+    return (expr,), ()
+
+
 def cmds_for_instantiable(x):
     a_id = get_absorbed_id(x)
     raet = x | without_absorbed | collect
@@ -508,7 +484,7 @@ def cmds_for_mergable(x):
         cmd['internal_ids'] += [a_id]
 
     if is_a(x, ZefRef) or is_a(x, EZefRef):
-        if BT(x) == BT.ENTITY_NODE:
+        if BT(x) in {BT.ENTITY_NODE, BT.TX_EVENT_NODE, BT.ROOT_NODE}:
             return (), [cmd]
 
         elif BT(x) == BT.ATOMIC_ENTITY_NODE:
@@ -542,18 +518,21 @@ def cmds_for_mergable(x):
             [cmd],
         )
     else:
-        raise NotImplementedError(f"Unknown type for merged[]: {type(x)}")
+        raise NotImplementedError(f"Unknown type for mergable: {type(x)}")
 
-def cmds_for_tag(x):
-    target = x.el_ops[0][1][0]
+def cmds_for_lv_tag(x, gen_id):
+    assert isinstance(x, LazyValue)
+
+    assert len(x | peel | collect) == 2
+    target,op = x | peel | collect
+
+    assert is_a(op, tag)
+    tag_s = LazyValue(op) | absorbed | single | collect
+    iid,exprs = realise_single_node(target, gen_id)
     cmd = {'cmd': 'tag',
-           'tag_name': x.el_ops[0][1][1],
-           'force': x.el_ops[0][1][2] if len(x.el_ops[0][1]) >= 3 else False}
-    if is_a(target, ZefOp) and is_a(target, Z):
-        cmd['internal_id'] = target.el_ops[0][1][0]
-    elif is_a(target, ZefRef) or is_a(target, EZefRef):
-        cmd['origin_rae'] = origin_rae(target)
-    return (), [cmd]
+           'tag_name': tag_s,
+           'internal_id': iid}
+    return exprs, [cmd]
     
 
 def cmds_for_lv_assign_value(x, gen_id: Callable):
@@ -586,9 +565,15 @@ def cmds_for_lv_terminate(x):
     return (), [cmd]
 
 def cmds_for_delegate(x):                            
+    internal_ids = []
+    a_id = get_absorbed_id(x)
+    if a_id is not None:
+        internal_ids += [a_id]
+
     return (), [{
         'cmd': 'merge', 
         'origin_rae': x,
+        'internal_ids': internal_ids,
         }]
 
 
@@ -654,76 +639,6 @@ def cmds_for_tuple(x: tuple, gen_id: Callable):
 
 
 
-#### ** Old versions
-def cmds_for_instantiated(x: dict):
-    internal_id = x.el_ops[0][1][1]
-    raet = x.el_ops[0][1][0]
-    cmds = []
-    if isinstance(raet, QuantityFloat) or isinstance(raet, QuantityInt):
-        # This duplicates the logic in realise_single_node a bit here
-        val = raet
-        if isinstance(val, QuantityFloat):
-            raet = getattr(AET.QuantityFloat, val.unit.enum_value)
-        else:
-            raet = getattr(AET.QuantityInt, val.unit.enum_value)
-        exprs += [Z[internal_id] | assign_value[val]]#{'cmd': 'assign_value', 'value': val, 'explicit': True, 'internal_id': internal_id}]
-
-    cmds += [{'cmd': 'instantiate', 'rae_type': raet, 'internal_id': internal_id}]
-    return (), cmds
-
-def cmds_for_merged(x: dict):
-    obj = x.el_ops[0][1][0]
-
-    origin = origin_rae(obj)
-
-    base_cmd = {"cmd": "merge",
-                "origin_rae": origin,
-                "internal_ids": []}
-    if len(x.el_ops[0][1]) == 2:
-        base_cmd["internal_ids"] += [x.el_ops[0][1][1]]
-    else:
-        assert len(x.el_ops[0][1]) == 1
-
-    if is_a(obj, ZefRef) or is_a(obj, EZefRef):
-        if BT(obj) == BT.ENTITY_NODE:
-            return (), [base_cmd]
-
-        elif BT(obj) == BT.ATOMIC_ENTITY_NODE:
-            assign_val_cmds = [] if isinstance(obj, EZefRef) else [{
-                'cmd': 'assign_value', 
-                'value': obj | value | collect,
-                'internal_id': uid(origin),
-                'explicit': False,
-                }]
-
-            return (), [base_cmd] + assign_val_cmds
-
-        elif BT(obj) == BT.RELATION_EDGE:
-            return (
-                (obj | source | to_ezefref | collect, obj | target | to_ezefref | collect),
-                [base_cmd],
-            )
-        else:
-            raise NotImplementedError(f"Unknown ZefRef type for merging: {BT(obj)}")
-    elif is_a(obj, Entity) or is_a(obj, AtomicEntity):
-        return (), [base_cmd]
-    elif is_a(obj, Relation):
-        maybe_src_trg = []
-        if not is_a(obj.d["type"][0], RT):
-            maybe_src_trg.append(source(obj))
-        if not is_a(obj.d["type"][2], RT):
-            maybe_src_trg.append(target(obj))
-
-        return (
-            tuple(maybe_src_trg),
-            [base_cmd],
-        )
-    else:
-        raise NotImplementedError(f"Unknown type for merged[]: {type(obj)}")
-        
-
-
-
 ##############################################################
 # ** Complex->primitve expr utils
 #------------------------------------------------------------
@@ -806,6 +721,9 @@ def realise_single_node(x, gen_id):
         elif is_a(op, terminate):
             iid = origin_uid(target)
             exprs = [x]
+        elif is_a(op, tag):
+            iid = origin_uid(target)
+            exprs = [x]
         else:
             raise Exception(f"Don't understand LazyValue type: {op}")
     elif isinstance(x, EntityType) or isinstance(x, AtomicEntityType):
@@ -824,8 +742,8 @@ def realise_single_node(x, gen_id):
         else:
             exprs = [x]
             iid = origin_uid(x)
-    elif type(x) in [Entity, AtomicEntity, Relation]:
-        exprs = [merged[x]]
+    elif type(x) in [Entity, AtomicEntity, Relation, TXNode, Root]:
+        exprs = [x]
         iid = origin_uid(x)
     elif type(x) in scalar_types:
         iid = gen_id()
@@ -837,24 +755,6 @@ def realise_single_node(x, gen_id):
             iid = params | first | collect
             # No expr to perform
             exprs = []
-        elif is_a(x, instantiated):
-            if length(params) == 1:
-                # We need to create a new id for this object
-                x = x[gen_id()]
-                params = LazyValue(x) | peel | first | second
-            iid = params | second | collect
-            exprs = [x]
-        elif is_a(x, merged):
-            # Note: we always use the origin_uid here, even if there's an
-            # internal id curried in. Simpler to later order operations.
-            iid = origin_uid(params | first | collect)
-            exprs = [x]
-        # elif is_a(x, assign_value):
-        #     iid = origin_uid(params | first | collect)
-        #     exprs = [x]
-        # elif is_a(x, terminate):
-        #     iid = origin_uid(params | first | collect)
-        #     exprs = [x]
         else:
             raise NotImplementedError(f"Can't pass zefops to GraphDelta: for {x}")
     elif is_a(x, Delegate):
@@ -1038,7 +938,7 @@ def can_be_executed(cmd):
             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) == get_id(other) for other in state['input'] if other['cmd'] != 'terminate'):
+        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        
     (_,can), (_,cannot) = state['input'] | group_by[can_be_executed][[True, False]] | collect
@@ -1141,10 +1041,6 @@ def step(x, allow_scalar):
             if not allow_scalar:
                 raise Exception("Scalars are not allowed on their own to avoid accidental typos such as (ET.X, ET.Y, 'z') when (ET.X, RT.Y, 'z') is meant. If you want this behaviour, then create an explicit AET, i.e. (AET.String <= 'z').")
         
-        if isinstance(x, ZefOp):
-            if is_a(x, instantiated):
-                raise Exception("Not allowed to pass internal names to RAEs in shorthand syntax")
-
         iid,exprs = realise_single_node(x, gen_id)
         gd_exprs.extend(exprs)
         return iid
@@ -1159,7 +1055,7 @@ def step(x):
             return tuple((step(el) for el in x))
         if isinstance(x, list):
             return [step(el) for el in x]
-        return receipt[x] if isinstance(x, str) or is_a(x, uid) else x
+        return receipt[x] if isinstance(x, str) or is_a(x, uid) or is_a(x, Delegate) else x
     return step(unpacking_template)
 
 
@@ -1204,12 +1100,12 @@ def perform_transaction_commands(commands: list, g: Graph):
                     if is_a(cmd['origin_rae'], Delegate):
                         d = cmd['origin_rae']
                         zz = internals.delegate_to_ezr(d, g, True, 0)
-                        d_raes[d] = zz
+                        zz = now(zz)
                     else:
                         candidate = most_recent_rae_on_graph(uid(cmd['origin_rae']), g)
                         if candidate is not None:
                             # this is already on the graph. Just assert and move on
-                            assert rae_type(cmd['origin_rae']) == rae_type(candidate)
+                            assert abstract_type(cmd['origin_rae']) == abstract_type(candidate)
                             zz = candidate
                         else:
                             origin_rae_uid = uid(cmd['origin_rae'])
@@ -1242,6 +1138,9 @@ def perform_transaction_commands(commands: list, g: Graph):
                                     origin_rae_uid.blob_uid,
                                     origin_rae_uid.graph_uid,
                                 )
+                            # Special handling for TXNode and Root currently.
+                            elif isinstance(cmd['origin_rae'], (TXNode, Root)):
+                                raise Exception("Can only merge TXNode and Root onto the same graph")
                             else:
                                 raise NotImplementedError
 
@@ -1269,7 +1168,7 @@ def perform_transaction_commands(commands: list, g: Graph):
                     else:
                         zz = d_raes[cmd['internal_id']]
                     # Go directly to the tagging instead of through another effect
-                    pyzef.main.tag(zz, cmd['tag_name'], cmd['force'])
+                    pyzef.main.tag(zz, cmd['tag_name'], True)
                 else:
                     raise RuntimeError(f'---------Unexpected case in performing graph delta tx: {cmd}')
 
@@ -1362,6 +1261,8 @@ def most_recent_rae_on_graph(origin_uid: str, g: Graph)->ZefRef:
             return zz | in_frame[now(g)] | collect
         else:
             return None
+    elif BT(zz) in {BT.ROOT_NODE, BT.TX_EVENT_NODE}:
+        return zz | in_frame[now(g)] | collect
     else:
         raise RuntimeError("Unexpected option in most_recent_rae_on_graph")
         
diff --git a/python/zef/core/graph_slice.py b/python/zef/core/graph_slice.py
index 98beb9fc..15c4430d 100644
--- a/python/zef/core/graph_slice.py
+++ b/python/zef/core/graph_slice.py
@@ -84,7 +84,7 @@ def __getitem__(self, thing):
         # We magically transform any FOREIGN_ENTITY_NODE accesses to the real RAEs.
         # Accessing the low-level BTs can only be done through traversals
         if BT(ezr) in [BT.FOREIGN_ENTITY_NODE, BT.FOREIGN_ATOMIC_ENTITY_NODE, BT.FOREIGN_RELATION_EDGE]:
-            res = get_instance_rae(uid(ezr), self)
+            res = get_instance_rae(uid(thing), self)
             if res is None:
                 raise KeyError("RAE doesn't have an alive instance in this timeslice")
             return res
diff --git a/python/zef/core/internals/__init__.py b/python/zef/core/internals/__init__.py
index d2cde41a..d522d553 100644
--- a/python/zef/core/internals/__init__.py
+++ b/python/zef/core/internals/__init__.py
@@ -120,6 +120,8 @@
 BlobType.__str__ = lambda self: repr(self).split(':')[0][10:]   # strip away the 'BlobType. ...' and out <>
 
 from .rel_ent_classes import ET, RT, EN, AET
+from .merges import register_merge_handler
+register_merge_handler()
 
 BT = BlobTypeStruct()
 
@@ -133,7 +135,7 @@ def safe_current_task():
 global_transaction_task = {}
 from contextlib import contextmanager
 @contextmanager
-def Transaction(g, wait=None, *, warn_exception=True):
+def Transaction(g, wait=None):
     global global_transaction_task
     from ...pyzef.zefops import uid
 
@@ -147,11 +149,6 @@ def Transaction(g, wait=None, *, warn_exception=True):
     current_tx = StartTransactionReturnTx(g)
     try:
         yield current_tx
-    except Exception as exc:
-        if warn_exception:
-            import logging
-            logging.error("Got an exception inside of a transaction, reraising. Exc: " + str(exc))
-        raise
     finally:
         if prev_val is None and uid(g) in global_transaction_task:
             del global_transaction_task[uid(g)]
@@ -177,45 +174,3 @@ def assign_value_imp(z, value):
         else:
             raise Exception(f"Don't know how to serialize a type of {val}")
     c_assign_value(z, value)
-
-####################################
-# * Old: deprecated
-#----------------------------------
-
-
-@contextmanager
-def expose_to(g, target):
-    # In all cases, sync the graph
-    from .. import sync
-    sync(g, True)
-
-    # This is for the convenience of not duplicating code by having to write a no-op context manager
-    if target is None:
-        yield
-        return
-
-    # Special case for username of everyone for convenience
-    if target == "everyone":
-        target = "group:everyone"
-    # Make the assumption that a user is meant, unless group is explicitly specified.
-    if ":" not in target:
-        target = "user:" + target
-
-    from ..admins import add_right, remove_right
-    from ..zefops import uid
-
-    add_right(target, "graph:" + uid(g), "AllowView")
-    add_right(target, "graph:" + uid(g), "AllowDiscover")
-
-    try:
-        yield
-    finally:
-        try:
-            remove_right(target, "graph:" + uid(g), "AllowView")
-            remove_right(target, "graph:" + uid(g), "AllowDiscover")
-        except:
-            import logging
-            logging.error("Ignoring error in finally block of expose_to when trying to remove rights from exposed graph.")
-
-
-    
diff --git a/python/zef/core/internals/merges.py b/python/zef/core/internals/merges.py
index 772bd7f5..f543d1b2 100644
--- a/python/zef/core/internals/merges.py
+++ b/python/zef/core/internals/merges.py
@@ -13,24 +13,20 @@
 # limitations under the License.
 
 
-def _graphdelta_merge(g, serialized_delta):
+def c_merge_handler(g, serialized_delta):
     """Don't call this explicitly. Only for the zefdb core."""
 
     from ..serialization import serialize, deserialize
-    from .. import Graph, Effect, FX
     from .._ops import run, transact
 
-    # Double check primary role here.
-    # if g.
-    # TODO The reason we are constructing an effect is that serialized 
-    # data contains the commands and not the actions.
-    # Can we somehow fix that?
+    # TODO: Double check primary role here.
+
     commands = deserialize(serialized_delta)
-    # receipt = delta | transact[g] | run
-    receipt = Effect({
-            "type": FX.TX.Transact,
-            "target_graph": g,
-            "commands": commands
-    }) | run
+    from zef.core.graph_delta import perform_transaction_commands
+    receipt = perform_transaction_commands(commands, g)
 
     return serialize(receipt)
+
+def register_merge_handler():
+    from ...pyzef import internals
+    internals.register_merge_handler(c_merge_handler)
diff --git a/python/zef/core/op_implementations/dispatch_dictionary.py b/python/zef/core/op_implementations/dispatch_dictionary.py
index a83dbe2c..f2a05a12 100644
--- a/python/zef/core/op_implementations/dispatch_dictionary.py
+++ b/python/zef/core/op_implementations/dispatch_dictionary.py
@@ -141,6 +141,7 @@
         RT.Keys:            (keys_imp, keys_tp),
         RT.Reverse:         (reverse_imp, reverse_tp),
         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.Blobs:           (blobs_imp, blobs_tp),
         RT.Z:               (Z_imp, Z_tp),
diff --git a/python/zef/core/op_implementations/implementation_typing_functions.py b/python/zef/core/op_implementations/implementation_typing_functions.py
index 940e9511..78484c4b 100644
--- a/python/zef/core/op_implementations/implementation_typing_functions.py
+++ b/python/zef/core/op_implementations/implementation_typing_functions.py
@@ -17,7 +17,7 @@
 
 
 from rx import operators as rxops
-from ..VT.value_type import ValueType
+from ..VT.value_type import ValueType_
 
 # This is the only submodule that is allowed to do this. It can assume that everything else has been made available so that it functions as a "user" of the core module.
 from .. import *
@@ -40,7 +40,7 @@ def curry_args_in_zefop(zefop, first_arg, nargs = ()):
     for arg in nargs: zefop = zefop[arg]
     return zefop(first_arg) if callable(zefop) else zefop[first_arg]
 
-def parse_input_type(input_type: ValueType) -> str:
+def parse_input_type(input_type: ValueType_) -> str:
     # TODO instead of returning a string may be return also types i.e VT.Zef or VT.Tools
     if input_type is None: return None
     if input_type == VT.Nil: return None
@@ -340,7 +340,7 @@ def match_apply_tp(op, curr_type):
 #---------------------------------------- peel -----------------------------------------------
 def peel_imp(el, *args):
     from ..fx.fx_types import _Effect_Class
-    if isinstance(el, ValueType):
+    if isinstance(el, ValueType_):
         return el.nested()
     elif isinstance(el, _Effect_Class):
         return el.d
@@ -361,7 +361,7 @@ def peel_tp(op, curr_type):
     elif curr_type in {VT.ZefOp, VT.LazyValue}:
         return VT.Record
     else:
-        return curr_type.nested()
+        return absorbed(curr_type)[0]
 
 
 #---------------------------------------- zip -----------------------------------------------
@@ -438,7 +438,7 @@ def concat_type_info(op, curr_type):
         curr_type = downing_d[curr_type]
     else:
         try:
-            curr_type = curr_type.nested()
+            curr_type = absorbed(curr_type)[0]
         except AttributeError as e:
             raise Exception(f"An operator that downs the degree of a Nestable object was called on a Degree-0 Object {curr_type}: {e}")
     return curr_type
@@ -622,6 +622,7 @@ def get_in_imp(d: dict, path, default_val=VT.Error):
     """
     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)
 
     
@@ -1128,6 +1129,11 @@ def get_imp(d, key, default=Error('Key not found in "get"')):
         return d.get(key, default)
     elif isinstance(d, list) or isinstance(d, tuple) or isinstance(d, Generator):
         return Error(f"get called on a list. Use 'nth' to get an element at a specified index.")
+    elif isinstance(d, Graph) or isinstance(d, GraphSlice):
+        try:
+            return d[key]
+        except KeyError:
+            return default
     else:
         return Error(f"get called with unsupported type {type(d)}.")
 
@@ -1672,7 +1678,7 @@ def absorbed_imp(x):
     - related zefop: inject_list
     - related zefop: reverse_args
     """
-    if isinstance(x, EntityType) or isinstance(x, RelationType) or isinstance(x, AtomicEntityType)  or isinstance(x, Keyword):
+    if isinstance(x, (EntityType, RelationType, AtomicEntityType, Keyword, Delegate)):
         if '_absorbed' not in x.__dict__:
             return ()
         else:
@@ -1689,7 +1695,7 @@ def absorbed_imp(x):
     elif isinstance(x, ZefRef) or isinstance(x, EZefRef):
         return ()
     
-    elif isinstance(x, ValueType):
+    elif isinstance(x, ValueType_):
         return x.d['absorbed']
 
     else:
@@ -1716,7 +1722,6 @@ def without_absorbed_imp(x):
     - related zefop: inject_list
     - related zefop: reverse_args
     """
-    print(f"without_absorbed called for {x=}")
     if isinstance(x, EntityType):
         if '_absorbed' not in x.__dict__:
             return x
@@ -1746,8 +1751,8 @@ def without_absorbed_imp(x):
             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=}')
         return ZefOp( ((x.el_ops[0][0], ()),) )
 
-    elif isinstance(x, ValueType):
-        return ValueType(type_name=x.d['type_name'])
+    elif isinstance(x, ValueType_):
+        return ValueType_(type_name=x.d['type_name'])
 
     return Error('Not Implemented')
 
@@ -2359,7 +2364,7 @@ def single_tp(op, curr_type):
         curr_type = downing_d[curr_type]
     else:
         try:
-            curr_type = curr_type.nested()
+            curr_type = absorbed(curr_type)[0]
         except AttributeError as e:
             raise Exception(f"An operator that downs the degree of a Nestable object was called on a Degree-0 Object {curr_type}: {e}")
     return curr_type
@@ -2423,7 +2428,7 @@ def first_tp(op, curr_type):
         curr_type = downing_d[curr_type]
     else:
         try:
-            curr_type = curr_type.nested()
+            curr_type = absorbed(curr_type)[0]
         except AttributeError as e:
             raise Exception(f"An operator that downs the degree of a Nestable object was called on a Degree-0 Object {curr_type}: {e}")
     return curr_type
@@ -2461,7 +2466,7 @@ def second_tp(op, curr_type):
         curr_type = downing_d[curr_type]
     else:
         try:
-            curr_type = curr_type.nested()
+            curr_type = absorbed(curr_type)[0]
         except AttributeError as e:
             raise Exception(f"An operator that downs the degree of a Nestable object was called on a Degree-0 Object {curr_type}: {e}")
     return curr_type
@@ -2503,7 +2508,7 @@ def last_tp(op, curr_type):
         curr_type = downing_d[curr_type]
     else:
         try:
-            curr_type = curr_type.nested()
+            curr_type = absorbed(curr_type)[0]
         except AttributeError as e:
             raise Exception(f"An operator that downs the degree of a Nestable object was called on a Degree-0 Object {curr_type}: {e}")
     return curr_type
@@ -2556,9 +2561,10 @@ def Z_tp(op, curr_type):
 
 
 #---------------------------------------- Root -----------------------------------------------
-def root_imp(g):
-    assert isinstance(g, Graph)
-    return g[42]            # TODO: don't hard code
+def root_imp(x):
+    if isinstance(x, (Graph, GraphSlice)):
+        return x[pyzef.internals.root_node_blob_index()]
+    raise Exception(f"Don't know how to find root of type {type(g)}")
 
 
 def root_tp(op, curr_type):
@@ -3057,7 +3063,7 @@ def tx_imp(*args):
     """
     import traceback
     traceback.print_stack()
-    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"😭😭😭😭😭😭😭😭 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]=}")
     # print(f">>>>> {args[1]=}")
     # print(f">>>>> {args[1]== lazy_zefops.instantiated}")
@@ -3085,7 +3091,7 @@ def tx_tp(op, curr_type):
     if curr_type == VT.Graph:
         curr_type = VT.EZefRefs
     else:
-        curr_type = VT.ZefRefs if "ZefRef" in curr_type.d[0] else VT.EZefRefs
+        curr_type = VT.ZefRefs if "ZefRef" in curr_type.d['type_name'] else VT.EZefRefs
     return curr_type
 
 
@@ -3191,6 +3197,7 @@ def time_slice_implementation(first_arg, *curried_args):
     ---- Signature ----
     GraphSlice -> Int
     """
+    print(f"😞😞😞😞😞  The use of the 'time_slice' is deprecated: use 'to_graph_slice' instead 😞😞😞😞😞")  
     if isinstance(first_arg, GraphSlice):
         return int(first_arg)       # casting a GraphSlice onto an int implemented
 
@@ -3295,6 +3302,7 @@ def instantiated_imp(x):
     ZefRef[RAE]    -> ZefRef[TX]
     EZefRef[RAE]   -> EZefRef[TX]
     """
+    print(f"Warning: 'instantiated' ZefOp will be retired 🥸🥸🥸🥸🥸🥸🥸🥸. Use 'z_tx | events[Terminated]' instead")
     if isinstance(x, GraphSlice):
         raise TypeError("`instantiated` cannot be used with a GraphSlice. Please convert to a TX first. For example `g | now | to_tx | instantiated`")
     assert isinstance(x, ZefRef) or isinstance(x, EZefRef)
@@ -3323,6 +3331,7 @@ def terminated_imp(x):
     ZefRef[RAE]    -> Union[ZefRef[TX], Nil]
     EZefRef[RAE]   -> Union[EZefRef[TX], Nil]
     """
+    print(f"Warning: 'terminated' ZefOp will be retired 🥸🥸🥸🥸🥸🥸🥸🥸. Use 'z_tx | events[Terminated]' instead")
     if isinstance(x, GraphSlice):
         raise TypeError("`terminated` cannot be used with a GraphSlice. Please convert to a TX first. For example `g | now | to_tx | terminated`")
     assert isinstance(x, ZefRef) or isinstance(x, EZefRef)
@@ -3354,6 +3363,7 @@ def value_assigned_imp(x):
     ZefRef[AET]  -> List[ZefRef[TX]]
     EZefRef[AET] -> List[EZefRef[TX]]
     """
+    print(f"Warning: 'value_assigned' ZefOp will be retired 🥸🥸🥸🥸🥸🥸🥸🥸. Use 'z_tx | events[Assigned]' instead")
     if isinstance(x, GraphSlice):
         raise TypeError("`value_assigned` cannot be used with a GraphSlice. Please convert to a TX first. For example `g | now | to_tx | value_assigned`")
     assert isinstance(x, ZefRef) or isinstance(x, EZefRef)
@@ -3388,6 +3398,7 @@ def merged_imp(x):
     ZefRef     -> GraphSlice
     GraphSlice -> List[ZefRef]
     """
+    print(f"Warning: 'merged' ZefOp will be retired 🥸🥸🥸🥸🥸🥸🥸🥸")
     if isinstance(x, GraphSlice):
         raise TypeError("`merged` cannot be used with a GraphSlice. Please convert to a TX first. For example `g | now | to_tx | merged`")
     assert isinstance(x, ZefRef) or isinstance(x, EZefRef)
@@ -3431,7 +3442,7 @@ def affected_imp(x):
     EZefRef[TX]  -> List[EZefRef]
     """
     # assert isinstance(x, GraphSlice)
-
+    print(f"Warning: 'affected' ZefOp will be retired 🥸🥸🥸🥸🥸🥸🥸🥸")
     return concat(instantiated(x), terminated(x), value_assigned(x))
 
 def affected_tp(x):
@@ -3441,28 +3452,62 @@ def affected_tp(x):
 
 # ----------------------------------------- events --------------------------------------------
 
-def events_imp(z_tx):
+def events_imp(z_tx_or_rae, filter_on=None):
     """ 
-    Given a TX as a (E)ZefRef, return all events that occurred.
+    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 ----
-    z_tx | events            # => [instantiated[z1], terminated[z3], value_assigned[z8][42] ]
+    >>> 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]]
 
     ---- Signature ----
     Union[ZefRef[TX], EZefRef[TX]]  ->  List[ZefOp[Union[Instantiated[ZefRef], Terminated[ZefRef], ValueAssigned[ZefRef, T]]]]
     """
-    z = to_ezefref(z_tx)
-    assert BT(z) == BT.TX_EVENT_NODE
-    gs = z | to_graph_slice | collect
+    from zef.pyzef import zefops as pyzefops
+
+    if BT(z_tx_or_rae) == BT.TX_EVENT_NODE:
+        zr = to_ezefref(z_tx_or_rae)
+        gs = zr | to_graph_slice | collect
+
+        def make_val_as_for_aet(aet):
+            aet_at_frame = to_frame[gs](aet)
+            try:
+                prev_tx  = zr | previous_tx | collect                                  # Will fail if tx is already first TX
+                prev_val = aet | to_frame[to_graph_slice(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)]
+
+        insts        = zr | pyzefops.instantiated   | map[lambda zz: instantiated[to_frame[gs](zz) ]] | collect
+        val_assigns  = zr | pyzefops.value_assigned | map[make_val_as_for_aet] | collect
+        terminations = zr | pyzefops.terminated     | map[lambda zz: terminated[to_frame[allow_tombstone][gs](zz) ]] | collect
+        full_list = insts+val_assigns+terminations
+    else:
+        zr = z_tx_or_rae
+
+        def make_val_as_from_tx(tx):
+            aet_at_frame = to_frame[to_graph_slice(tx)](zr)
+            try:
+                prev_tx  = tx | previous_tx | collect                                   # Will fail if tx is already first TX
+                prev_val = zr | to_frame[to_graph_slice(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)]
 
-    def make_val_ev(zz):
-        zzz = to_frame[gs](zz)
-        return value_assigned[zzz][value(zzz)]
+        inst        =  [pyzefops.instantiation_tx(zr)]   | map[lambda tx: instantiated[to_frame[to_graph_slice(tx)](zr) ]] | 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[to_frame[allow_tombstone][to_graph_slice(tx)](zr)]] | collect
+        full_list = inst + val_assigns + termination 
 
-    insts = z | instantiated | map[lambda zz: instantiated[to_frame[gs](zz) ]] | collect
-    val_assigns = z | value_assigned | map[make_val_ev] | collect
-    terminations = z | terminated | map[lambda zz: terminated[to_frame[allow_tombstone][gs](zz) ]] | collect
-    return insts+val_assigns+terminations
+    if filter_on: return full_list | filter[lambda z: is_a(z, filter_on)] | collect
+    return full_list
 
 
 def events_tp(x):
@@ -3533,14 +3578,18 @@ def in_frame_imp(z, *args):
     zz = to_ezefref(z)
     g_frame = Graph(z_tx)
     # z's origin lives in the frame graph
-    cond1 = g_frame == Graph(zz)    # will be reused
-    if cond1 or origin_uid(zz).graph_uid == uid(g_frame):
-        z_obj = to_ezefref(z) if cond1 else g_frame[origin_uid(zz)]
+    is_same_g = g_frame == Graph(zz)    # will be reused
+    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 internals.is_delegate(z_obj):
             from ..logger import log
             log.warn("Need to fix, assuming delegate exists from the beginning of time.")
             instantiation_ts = 0
+        elif BT(z_obj) == BT.TX_EVENT_NODE:
+            instantiation_ts = time_slice(z_obj)
+        elif BT(z_obj) == BT.ROOT_NODE:
+            instantiation_ts = 0
         else:
             instantiation_ts = time_slice(instantiated(z_obj))
         if (time_slice(z_tx)) < instantiation_ts:
@@ -3551,6 +3600,8 @@ def in_frame_imp(z, *args):
                 from ..logger import log
                 log.warn("Need to fix, assuming delegate exists to the end of time.")
                 z_tx_term = None
+            elif BT(z_obj) in {BT.TX_EVENT_NODE, BT.ROOT_NODE}:
+                z_tx_term = None
             else:
                 z_tx_term = terminated(z_obj)
             if z_tx_term is not None and index(z_tx_term)!=internals.root_node_blob_index():  # if there is a termination tx
@@ -3762,11 +3813,13 @@ def time_travel_tp(x, p):
 
 def origin_uid_imp(z) -> EternalUID:
     """used in constructing GraphDelta, could be useful elsewhere"""
-    if type(z) in [Entity, AtomicEntity, Relation]:
+    if type(z) in [Entity, AtomicEntity, Relation, TXNode, Root]:
         return uid(z)
-    assert BT(z) in {BT.ENTITY_NODE, BT.ATOMIC_ENTITY_NODE, BT.RELATION_EDGE}
+    assert BT(z) in {BT.ENTITY_NODE, BT.ATOMIC_ENTITY_NODE, BT.RELATION_EDGE, BT.TX_EVENT_NODE, BT.ROOT_NODE}
     if internals.is_delegate(z):
         return uid(to_ezefref(z))
+    if BT(z) in {BT.TX_EVENT_NODE, BT.ROOT_NODE}:
+        return uid(to_ezefref(z))
     origin_candidates = z | to_ezefref | in_rel[BT.RAE_INSTANCE_EDGE] | Outs[BT.ORIGIN_RAE_EDGE] | collect    
     if len(origin_candidates) == 0:
         # z itself is the origin
@@ -3790,7 +3843,7 @@ def origin_uid_tp(x):
 
 def origin_rae_imp(x):
     """For RAEs, return an abstract entity, relation or atomic entity. For delegates, acts as the identity.""" 
-    if type(x) in [Entity, AtomicEntity, Relation]:
+    if type(x) in [Entity, AtomicEntity, Relation, TXNode, Root]:
         return x
     if isinstance(x, ZefRef) or isinstance(x, EZefRef):
         if internals.is_delegate(x):
@@ -3801,6 +3854,10 @@ def origin_rae_imp(x):
             return Relation(x)
         elif BT(x) == BT.ATOMIC_ENTITY_NODE:
             return AtomicEntity(x)
+        elif BT(x) == BT.TX_EVENT_NODE:
+            return TXNode(x)
+        elif BT(x) == BT.ROOT_NODE:
+            return Root(x)
         raise Exception("Not a ZefRef that is a concrete RAE")
     if is_a_implementation(x, Delegate):
         return x
@@ -4587,9 +4644,11 @@ def time_implementation(x, *curried_args):
 
 
 def instantiation_tx_implementation(first_arg, *curried_args):
+    print(f"Warning: 'instantiation_tx' ZefOp will be retired 🥸🥸🥸🥸🥸🥸🥸🥸. Use 'z_rae | events[Instantiated]' instead")
     return (pyzefops.instantiation_tx)(first_arg, *curried_args)
 
 def termination_tx_implementation(first_arg, *curried_args):
+    print(f"Warning: 'termination_tx' ZefOp will be retired 🥸🥸🥸🥸🥸🥸🥸🥸. Use 'z_rae | events[Terminated]' instead")
     return (pyzefops.termination_tx)(first_arg, *curried_args)
 
 def instances_implementation(*args):
@@ -4599,14 +4658,13 @@ def instances_implementation(*args):
 def uid_implementation(arg):
     if isinstance(arg, str):
         return to_uid(arg)
-    if isinstance(arg, Entity):
+    if isinstance(arg, (Entity, AtomicEntity, TXNode, Root)):
         return arg.d["uid"]
     if isinstance(arg, Relation):
         return arg.d["uids"][1]
-    if isinstance(arg, AtomicEntity):
-        return arg.d["uid"]
-    else:
-        return pyzefops.uid(arg)
+    if is_a(arg, uid):
+        return arg
+    return pyzefops.uid(arg)
 
 def base_uid_implementation(first_arg):
     if isinstance(first_arg, EternalUID) or isinstance(first_arg, ZefRefUID):
@@ -4649,7 +4707,7 @@ def zef_type_imp(x):
     """
     Warning: this function is not complete and its behavior may change!!!!!!!!!!!
 
-    Returns a Zef ValueType (a value itself),
+    Returns a Zef ValueType_ (a value itself),
     also when used with the python builtin supported
     types, as well as with instances of Zef Values.
 
@@ -4659,6 +4717,7 @@ def zef_type_imp(x):
     Should the various uid types be exposed to userland?
 
     """
+    from ...core.bytes import Bytes_
     tp = type(x)    
     try:
         return {
@@ -4671,6 +4730,7 @@ def zef_type_imp(x):
             dict: VT.Dict,
             set: VT.Set,
             Time: VT.Time,
+            Bytes_: VT.Bytes,
             # QuantityFloat: VT.QuantityFloat,
             # QuantityInt: VT.QuantityInt,
             # EntityType: VT.EntityType,
@@ -4686,7 +4746,7 @@ def zef_type_imp(x):
             Graph: VT.Graph,
             # FlatGraph: VT.FlatGraph,
             # Keyword: VT.Keyword,
-            # ValueType: VT.ValueType,
+            # ValueType_: VT.ValueType_,
             # SymbolicExpression: VT.SymbolicExpression,
             # Stream: VT.Stream,
             # Error: VT.Error,
@@ -4721,8 +4781,8 @@ def intersection_matching(el, intersection):
     def setof_matching(el, setof):
         from typing import Callable
         for t in setof.d['absorbed']: 
-            if isinstance(t, ValueType): 
-                return Error.ValueError(f"A ValueType was passed to SetOf but it only takes predicate functions. Try wrapping in is_a[{t}]")
+            if isinstance(t, ValueType_): 
+                return Error.ValueError(f"A ValueType_ was passed to SetOf but it only takes predicate functions. Try wrapping in is_a[{t}]")
             elif isinstance(t, (ZefOp, Callable)):
                 if not t(el): return False
             else: return Error.ValueError(f"Expected a predicate function or a ZefOp type inside SetOf but got {t} instead.")
@@ -4731,7 +4791,7 @@ def setof_matching(el, setof):
     def valuetype_matching(el, vt):
         if vt == VT.Any: return True
 
-        # TODO: compare on actual ValueType along with its absorbed subtypes. 
+        # TODO: compare on actual ValueType_ along with its absorbed subtypes. 
         # TODO: Extend list
         vt_name_to_python_type = {
             "Nil": type(None),
@@ -4744,11 +4804,11 @@ def valuetype_matching(el, vt):
             "Dict": dict,
             "Set": set,
         }
-        if vt.d['type_name'] not in vt_name_to_python_type: return Error.NotImplementedError(f"ValueType matching not implemented for {vt}")
+        if vt.d['type_name'] not in vt_name_to_python_type: return Error.NotImplementedError(f"ValueType_ matching not implemented for {vt}")
         python_type = vt_name_to_python_type[vt.d['type_name']]
         return isinstance(el, python_type)
 
-    if isinstance(typ, ValueType):
+    if isinstance(typ, ValueType_):
         if typ.d['type_name'] == "Union":
             return union_matching(x, typ)
 
@@ -4758,9 +4818,14 @@ def valuetype_matching(el, vt):
         if typ.d['type_name'] == "SetOf":
             return setof_matching(x, typ)
 
+        if typ.d['type_name'] in  {"Instantiated", "Assigned", "Terminated"}:
+            map_ = {"Instantiated": instantiated, "Assigned": value_assigned, "Terminated": terminated}
+            return without_absorbed(x) == map_[typ.d['type_name']]
+            
         return valuetype_matching(x, typ)
 
 
+
     
 
     if type(x) == _ErrorType:
@@ -4909,6 +4974,16 @@ def rae_type_implementation(z):
         return z.d["type"]
     return pymain.rae_type(z)
 
+def abstract_type_implementation(z):
+    # This is basically rae_type, but also including TXNode and Root
+    if isinstance(z, TXNode):
+        return BT.TX_EVENT_NODE
+    if isinstance(z, Root):
+        return BT.ROOT_NODE
+    if isinstance(z, (ZefRef, EZefRef)) and BT(z) in {BT.TX_EVENT_NODE, BT.ROOT_NODE}:
+        return BT(z)
+    return rae_type(z)
+
 
 
 
@@ -4994,7 +5069,7 @@ def nth_type_info(op, curr_type):
         curr_type = downing_d[curr_type]
     else:
         try:
-            curr_type = curr_type.nested()
+            curr_type = absorbed(curr_type)[0]
         except AttributeError as e:
             raise Exception(f"An operator that downs the degree of a Nestable object was called on a Degree-0 Object {curr_type}: {e}")
     return curr_type
@@ -5010,13 +5085,13 @@ def sort_type_info(op, curr_type):
     return curr_type
 
 def ins_type_info(op, curr_type):
-    return VT.ZefRefs if "ZefRef" in curr_type.d[0] else VT.EZefRefs
+    return VT.ZefRefs if "ZefRef" in curr_type.d['type_name'] else VT.EZefRefs
 
 def outs_type_info(op, curr_type):
-    return VT.ZefRefs if "ZefRef" in curr_type.d[0] else VT.EZefRefs
+    return VT.ZefRefs if "ZefRef" in curr_type.d['type_name'] else VT.EZefRefs
 
 def ins_and_outs_type_info(op, curr_type):
-    return VT.ZefRefs if "ZefRef" in curr_type.d[0] else VT.EZefRefs
+    return VT.ZefRefs if "ZefRef" in curr_type.d['type_name'] else VT.EZefRefs
 
 def source_type_info(op, curr_type):
     return curr_type
@@ -5072,25 +5147,25 @@ def l_type_info(op, curr_type):
 def Out_type_info(op, curr_type):
     if op[1][0] == RT.L: 
         assert curr_type in zef_types # Can only be used with Zef types
-        curr_type = VT.ZefRefs if "ZefRef" in curr_type.d[0] else VT.EZefRefs
+        curr_type = VT.ZefRefs if "ZefRef" in curr_type.d['type_name'] else VT.EZefRefs
     return curr_type
 
 def In_type_info(op, curr_type):
     if op[1][0] == RT.L: 
         assert curr_type in zef_types # Can only be used with Zef types
-        curr_type = VT.ZefRefs if "ZefRef" in curr_type.d[0] else VT.EZefRefs
+        curr_type = VT.ZefRefs if "ZefRef" in curr_type.d['type_name'] else VT.EZefRefs
     return curr_type
 
 def OutOut_type_info(op, curr_type):
     if op[1][0] == RT.L: 
         assert curr_type in zef_types # Can only be used with Zef types
-        curr_type = VT.ZefRefs if "ZefRef" in curr_type.d[0] else VT.EZefRefs
+        curr_type = VT.ZefRefs if "ZefRef" in curr_type.d['type_name'] else VT.EZefRefs
     return curr_type
 
 def InIn_type_info(op, curr_type):
     if op[1][0] == RT.L: 
         assert curr_type in zef_types # Can only be used with Zef types
-        curr_type = VT.ZefRefs if "ZefRef" in curr_type.d[0] else VT.EZefRefs
+        curr_type = VT.ZefRefs if "ZefRef" in curr_type.d['type_name'] else VT.EZefRefs
     return curr_type
 
 def terminate_implementation(z, *args):
@@ -5166,6 +5241,9 @@ def hasin_type_info(op, curr_type):
 def rae_type_type_info(op, curr_type):
     return VT.BT
 
+def abstract_type_type_info(op, curr_type):
+    return VT.BT
+
 
 def is_represented_as_implementation(arg, vt):
     if isinstance(arg, bool):
@@ -5183,14 +5261,15 @@ def is_represented_as_implementation(arg, vt):
 def is_represented_as_type_info(op, curr_type):
     VT.Bool
 #---------------------------------------- tag -----------------------------------------------
-def tag_imp(x, tag: str, force: bool = False):
+def tag_imp(x, tag_s: str, *args):
     """ 
     Create an effect to add a tag to either a Graph or a RAE.
 
     ---- Examples ----
     >>> g | tag["my_graph"] | run
     >>> g2 | tag["my_graph"][True] | run
-    >>> z | tag["settings_node"] | run
+    >>> z | tag["settings_node"] | g | run
+    >>> [z | tag["settings_node"]] | transact[g] | run
 
     ---- Arguments ----
     x Graph / ZefRef: object to tag
@@ -5199,26 +5278,26 @@ def tag_imp(x, tag: str, force: bool = False):
 
     ---- Signature ----
     (Graph, str, bool) -> Effect
-    (ZefRef, str, bool) -> Effect
+    (ZefRef, str, bool) -> LazyValue
     """
     if isinstance(x, Graph):
+        if len(args) > 0:
+            assert len(args) == 1
+            force = args[0]
+        else:
+            force = False
         return Effect({
             'type': FX.Graph.Tag,
             'graph': x,
-            'tag': tag,
-            'force': force,
-            'adding': True,
-        })
-    if isinstance(x, ZefRef) or isinstance(x, EZefRef):
-        return Effect({
-            'type': FX.RAE.Tag,
-            'rae': to_ezefref(x),
-            'tag': tag,
+            'tag': tag_s,
             'force': force,
             'adding': True,
         })
+    if isinstance(x, ZefRef) or isinstance(x, EZefRef) or is_a(x, Z):
+        assert len(args) == 0
+        return LazyValue(x) | tag[tag_s]
 
-    raise RuntimeError("Unknonwn type for tag: {type(x)}")
+    raise RuntimeError(f"Unknown type for tag: {type(x)}")
     
     
 def tag_tp(op, curr_type):
@@ -5244,16 +5323,10 @@ def untag_imp(x, tag: str):
             'adding': False,
             'force': False,
         })
-    if isinstance(x, ZefRef) or isinstance(x, EZefRef):
-        return Effect({
-            'type': FX.RAE.Tag,
-            'rae': to_ezefref(x),
-            'tag': tag,
-            'adding': False,
-            'force': False,
-        })
+    if isinstance(x, ZefRef) or isinstance(x, EZefRef) or is_a(x, Z):
+        raise Exception("Untagging a RAE is not supported at the moment.")
 
-    raise RuntimeError("Unknonwn type for tag: {type(x)}")
+    raise RuntimeError(f"Unknown type for tag: {type(x)}")
     
     
 def untag_tp(op, curr_type):
@@ -5793,21 +5866,30 @@ def replace_at_imp(str_or_list, index, new_el):
     (VT.String, VT.Int, VT.String) -> VT.String
     (VT.List, VT.Int, VT.Any) -> VT.List
     """
+    from typing import Generator
     if isinstance(str_or_list, str):
         s = str_or_list
         char = str(new_el)
         if index >= len(s) or index < 0: return s
         if index == len(s) - 1: return s[:index] + char
         return s[:index] + char + s[index+1:] 
-    elif isinstance(str_or_list, list):
-        lst = str_or_list
-        if index >= len(lst) or (index < 0 and abs(index) > length(lst)): return lst
-        lst[index] = new_el
-        return lst
+    elif isinstance(str_or_list, list) or isinstance(str_or_list, tuple) or isinstance(str_or_list, Generator):
+        it = iter(str_or_list)
+        c = 0
+        try:
+            while c < index:
+                yield next(it)
+                c += 1
+            next(it)
+            yield new_el
+            yield from it
+        except StopIteration:
+            return
     else:
         return Error.TypeError(f"Expected an string or a list. Got {type(str_or_list)} instead.")
 
 
+
 def replace_at_tp(op, curr_type):
     return VT.String
 
diff --git a/python/zef/core/op_structs.py b/python/zef/core/op_structs.py
index 2c1c5173..83a1c644 100644
--- a/python/zef/core/op_structs.py
+++ b/python/zef/core/op_structs.py
@@ -229,14 +229,15 @@ def is_python_scalar_type(o):
 
 def is_supported_value(o):
     from .fx.fx_types import _Effect_Class
-    from .VT import ValueType
+    from .VT import ValueType_
     from . import  GraphSlice
     from types import GeneratorType
     from .. import Image
     from .error import _ErrorType
     from ..pyzef.main import Keyword
+    from ..core.bytes import Bytes_
     if is_python_scalar_type(o): return True
-    if type(o) in {set, range, GeneratorType, list, tuple, dict, _Effect_Class, ValueType, GraphSlice, Time, Image, _ErrorType, Keyword}: return True
+    if type(o) in {set, range, GeneratorType, list, tuple, dict, _Effect_Class, ValueType_, GraphSlice, Time, Image, Bytes_, _ErrorType, Keyword}: return True
     return False
 
 def is_supported_zef_value(o):
@@ -668,6 +669,7 @@ def evaluation(self, other, kind):
         from rx.core import Observable
         from .op_implementations.dispatch_dictionary import _op_to_functions
         from .fx import _state
+        from ._ops import absorbed, only
 
         curr_type =  VT.Awaitable[VT.List[VT.String]]
         source = _state['streams'][self.stream_ezefref]  
@@ -680,7 +682,7 @@ def evaluation(self, other, kind):
             for op in ops if kind == "Subscribe" else ops[:-1]:
                 observable_chain = _op_to_functions[op[0]][0](observable_chain,  *op[1])
 
-                curr_type = VT.Awaitable[_op_to_functions[op[0]][1](op,  curr_type.nested())]
+                curr_type = VT.Awaitable[_op_to_functions[op[0]][1](op,  *absorbed(curr_type))]
                 concrete_awaitable = ConcreteAwaitable(observable_chain, curr_type, concrete_awaitable.chain)
 
             if kind == "Subscribe":
@@ -693,13 +695,12 @@ def evaluation(self, other, kind):
             else:
                 observable_chain = observable_chain.pipe(rxops.to_list())
                 def type_assertion_wrapper(el):
-                    if isinstance(el, list) and curr_type.nested().d[0] != "List":
-                        from zefdb.lazy_zefops import only
+                    if isinstance(el, list) and absorbed(curr_type)[0] != "List":
                         return ops[-1][1][0](only(el))
                     return ops[-1][1][0](el)
                 observable_chain.subscribe(type_assertion_wrapper)
 
-            concrete_awaitable = ConcreteAwaitable(observable_chain, curr_type.nested(), concrete_awaitable.chain)
+            concrete_awaitable = ConcreteAwaitable(observable_chain, *absorbed(curr_type), concrete_awaitable.chain)
             return observable_chain 
         
         raise NotImplementedError(f"Awaitable evalation not implemented with {type(source)}")
@@ -1089,12 +1090,12 @@ def type_spec_tuple(obj):
         return new_tup[VT.Any]
 
 def type_spec(obj, no_type_casting = False):
-    from .VT import ValueType
+    from .VT import ValueType_
     from .fx.fx_types import _Effect_Class
     from . import GraphSlice
     from rx.subject import Subject
     from rx.core import Observable
-    if isinstance(obj, ValueType):               return obj
+    if isinstance(obj, ValueType_):               return obj
     if isinstance(obj, type) or no_type_casting: t = obj
     else:                                        t = type(obj)
     res = {
@@ -1119,7 +1120,7 @@ def type_spec(obj, no_type_casting = False):
         Observable:                 VT.Awaitable,
         _Effect_Class:              VT.Effect,
         GraphSlice:                 VT.GraphSlice,
-    }.get(t, lambda o: ValueType(type(o).__name__, 0))
+    }.get(t, lambda o: ValueType_(type(o).__name__, 0))
     try:
         return res if str(res) in dir(VT) else res(obj)
     except Exception as e:
diff --git a/python/zef/core/patching.py b/python/zef/core/patching.py
index a57f2f47..ac3b537c 100644
--- a/python/zef/core/patching.py
+++ b/python/zef/core/patching.py
@@ -28,6 +28,8 @@
 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)
 
 
 
@@ -141,54 +143,29 @@ def _rae_get_item(self, n):
 
 # ---------------------------- special treatment for EntityType ------------------------------------
 
-def entity_type_get_item(self, x):
+def absorbed_get_item(self, x):
     # don't mutate, create a new instance
-    new_et = main.EntityType(self.value)
+    from copy import copy
+    new_obj = copy(self)
     if '_absorbed' not in self.__dict__:
-        new_et._absorbed = (x, )
+        new_obj._absorbed = (x, )
     else:
-        new_et._absorbed = (*self._absorbed, x)
-    return new_et
-
-
-def relation_type_get_item(self, x):
-    # don't mutate, create a new instance
-    new_rt = main.RelationType(self.value)
-    if '_absorbed' not in self.__dict__:
-        new_rt._absorbed = (x, )
-    else:
-        new_rt._absorbed = (*self._absorbed, x)
-    return new_rt
-
-
-def atomic_entity_type_get_item(self, x):
-    # don't mutate, create a new instance
-    new_aet = internals.AtomicEntityType(self.value)
-    if '_absorbed' not in self.__dict__:
-        new_aet._absorbed = (x, )
-    else:
-        new_aet._absorbed = (*self._absorbed, x)
-    return new_aet
-
-
-def keyword_get_item(self, x):
-    # don't mutate, create a new instance
-    new_kw = main.Keyword(self.value)
-    if '_absorbed' not in self.__dict__:
-        new_kw._absorbed = (x, )
-    else:
-        new_kw._absorbed = (*self._absorbed, x)
-    return new_kw
-
-
+        new_obj._absorbed = (*self._absorbed, x)
+    return new_obj
+    
 
-def repr_with_absorbed(self):
-    original = self.__repr_without_absorbed__()
+def repr_with_absorbed(self, orig_repr):
+    original = orig_repr(self)
     if '_absorbed' not in self.__dict__:
         return original
     else:
         return original + ''.join(('[' + repr(el) + ']' for el in self._absorbed))
 
+def wrap_repr(typ):
+    orig = typ.__repr__
+    typ.__repr__ = lambda self,orig=orig: repr_with_absorbed(self, orig)
+    
+
 
 # The C++ implementation does not consider the absorbed values.
 # Monkeypatch this function here.
@@ -201,19 +178,21 @@ def keyword_eq_monkeypatched(self, other):
 
 
 
-main.EntityType.__getitem__ = entity_type_get_item
-main.EntityType.__repr__ = repr_with_absorbed
+main.EntityType.__getitem__ = absorbed_get_item
+wrap_repr(main.EntityType)
 
-main.RelationType.__getitem__ = relation_type_get_item
-main.RelationType.__repr__ = repr_with_absorbed
+main.RelationType.__getitem__ = absorbed_get_item
+wrap_repr(main.RelationType)
 
-main.Keyword.__getitem__ = keyword_get_item
-main.Keyword.__repr__ = repr_with_absorbed
+main.Keyword.__getitem__ = absorbed_get_item
+wrap_repr(main.Keyword)
 main.Keyword.__eq__ = keyword_eq_monkeypatched
 
-internals.AtomicEntityType.__getitem__ = atomic_entity_type_get_item
-internals.AtomicEntityType.__repr__ = repr_with_absorbed
+internals.AtomicEntityType.__getitem__ = absorbed_get_item
+wrap_repr(internals.AtomicEntityType)
 
+internals.Delegate.__getitem__ = absorbed_get_item
+wrap_repr(internals.Delegate)
 
 
 
@@ -305,4 +284,4 @@ def Graph__getitem__(self, x):
         pass
     return res
 
-main.Graph.__getitem__ = Graph__getitem__
\ No newline at end of file
+main.Graph.__getitem__ = Graph__getitem__
diff --git a/python/zef/gql/resolvers_utils.py b/python/zef/gql/resolvers_utils.py
index e61af1a8..7e71cf6e 100644
--- a/python/zef/gql/resolvers_utils.py
+++ b/python/zef/gql/resolvers_utils.py
@@ -95,7 +95,7 @@ def create_resolver_to_field(g, gql_rt, del_rt, is_out = False):
 
 def create_zef_function_resolver(g, gql_rt, z_func):
     if length(now(gql_rt) >> L[RT(gqlify("resolve_with_zef_function"))]) == 0:
-        instantiate(gql_rt, RT(gqlify("resolve_with_zef_function")), z_func, g)
+        (gql_rt, RT(gqlify("resolve_with_zef_function")), z_func) | g | run
 
 # Relay #
 def apply_cursor_to_edges(edges, before=None, after=None):
diff --git a/python/zef/gql/simplegql/generate_api.py b/python/zef/gql/simplegql/generate_api.py
index 256a6362..f4af3e7e 100644
--- a/python/zef/gql/simplegql/generate_api.py
+++ b/python/zef/gql/simplegql/generate_api.py
@@ -16,6 +16,7 @@
 from ... import *
 from ...ops import *
 from functools import partial as P
+from ...core.logger import log
 
 from ariadne import ObjectType, QueryType, MutationType, EnumType, ScalarType
 
@@ -35,6 +36,19 @@ def is_core_scalar(z):
 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_field = Assert[is_a[RT.GQL_Field]][lambda z: f"{z} is not a GQL field"]
 
+@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
+        
 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]]
@@ -46,6 +60,13 @@ def is_core_scalar(z):
 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_upfetch = assert_field >> O[RT.Upfetch] | value_or[False] | collect
+op_upfetch_field = (assert_type > L[RT.GQL_Field]
+                    | filter[op_is_upfetch]
+                    | single_or[None]
+                    | collect)
+op_has_upfetch = op_upfetch_field | Not[equals[None]] | collect
+
 ########################################
 # * Generating it all
 #--------------------------------------
@@ -77,6 +98,7 @@ def generate_resolvers_fcts(schema_root):
         field_schemas = []
         ref_field_schemas = []
         add_field_schemas = []
+        upfetch_field_schemas = []
         aggregate_outs = []
 
         for z_field in z_type > L[RT.GQL_Field]:
@@ -91,6 +113,14 @@ def generate_resolvers_fcts(schema_root):
             is_summable = z_field | target | op_is_orderable | collect
             is_aggregable = z_field | op_is_aggregable | collect
 
+            if op_is_unique(z_field):
+                assert is_scalar, "Unique fields must be scalars"
+                assert not is_list, "Unique fields can't be lists"
+
+            if op_is_upfetch(z_field):
+                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
             if is_scalar:
                 base_ref_field_type = base_field_type
@@ -104,16 +134,21 @@ def generate_resolvers_fcts(schema_root):
             if is_list:
                 maybe_params = "(" + schema_generate_list_params(target(z_field), extra_filters) + ")"
                 ref_field_type = "[" + base_ref_field_type + "]"
-                add_field_type = "[" + base_ref_field_type + maybe_required + "]"
+                # add_field_type = "[" + base_ref_field_type + maybe_required + "]"
+                add_field_type = ref_field_type
+                upfetch_field_type = "[" + base_ref_field_type + maybe_required + "]"
                 field_type = "[" + base_field_type + maybe_required + "]"
             else:
                 maybe_params = ""
                 ref_field_type = base_ref_field_type
-                add_field_type = base_ref_field_type + maybe_required
+                # add_field_type = base_ref_field_type + maybe_required
+                add_field_type = ref_field_type
+                upfetch_field_type = base_ref_field_type + maybe_required
                 field_type = base_field_type + maybe_required
             field_schemas += [f"{field_name}{maybe_params}: {field_type}"]
             add_field_schemas += [f"{field_name}: {add_field_type}"]
             ref_field_schemas += [f"{field_name}: {ref_field_type}"]
+            upfetch_field_schemas += [f"{field_name}: {upfetch_field_type}"]
 
             if is_aggregable:
                 if is_orderable:
@@ -128,6 +163,7 @@ def generate_resolvers_fcts(schema_root):
         field_schemas += ["id: ID!"]
         add_field_schemas += ["id: ID"]
         ref_field_schemas += ["id: ID"]
+        # Note: upfetch does not get an id field
         Type.set_field("id", resolve_id)
 
         # Add the 3 top-level queries
@@ -144,6 +180,10 @@ def generate_resolvers_fcts(schema_root):
                             P(resolve_update, type_node=z_type))
         Mutation.set_field(f"delete{name}",
                             P(resolve_delete, type_node=z_type))
+        has_upfetch = op_has_upfetch(z_type)
+        if has_upfetch:
+            Mutation.set_field(f"upfetch{name}",
+                               P(resolve_upfetch, type_node=z_type))
 
         MutateResponse = ObjectType(f"Mutate{name}Response")
         all_objects += [MutateResponse]
@@ -155,6 +195,7 @@ def generate_resolvers_fcts(schema_root):
         field_schemas = '\n\t'.join(field_schemas)
         ref_field_schemas = '\n\t'.join(ref_field_schemas)
         add_field_schemas = '\n\t'.join(add_field_schemas)
+        upfetch_field_schemas = '\n\t'.join(upfetch_field_schemas)
         aggregate_outs = '\n\t'.join(aggregate_outs)
 
         type_schemas += [f"""type {name} {{
@@ -185,6 +226,12 @@ def generate_resolvers_fcts(schema_root):
         {aggregate_outs}
 }}
 
+"""]
+        if has_upfetch:
+            type_schemas += [f"""
+input Upfetch{name}Input {{
+        {upfetch_field_schemas}
+}}
 """]
 
         # id_fields = z_type > L[RT.GQL_Field] | filter[Z >> O[RT.IsID] | value_or[False]]
@@ -202,7 +249,10 @@ def generate_resolvers_fcts(schema_root):
             f"update{name}(input: Update{name}Input!): Mutate{name}Response",
             f"delete{name}(filter: {name}Filter!): Mutate{name}Response",
         ]
-
+        if has_upfetch:
+            mutation_fields += [
+                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)
@@ -396,10 +446,13 @@ def schema_generate_scalar_filter(z_node):
 # * Query resolvers
 #----------------------------------
 
+class ExternalError(Exception):
+    pass
+
 def resolve_get(_, info, *, type_node, **params):
     # Look for something that fits exactly what has been given in the params, assuming
     # that ariadne has done its work and validated the query.
-    return find_existing_entity(info, type_node, params["id"])
+    return find_existing_entity_by_id(info, type_node, params["id"])
 
 def resolve_query(_, info, *, type_node, **params):
     g = info.context["g"]
@@ -476,74 +529,151 @@ def resolve_id(z, info):
 # * Mutations
 #----------------------------
 def resolve_add(_, info, *, type_node, **params):
-    name_gen = NameGen()
-    actions = []
-    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 Exception("Can't update item with id without setting upsert")
-            obj = find_existing_entity(info, type_node, item["id"])
+    # 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]
+
+        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)
+
+        return {"count": count, "ents": ents}
+    except ExternalError:
+        raise
+    except Exception as exc:
+        log.error("There was an error in resolve_add", exc_info=exc)
+        raise Exception("Unexpected error")
+            
+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:
-                raise Exception("Item doesn't exist")
-            set_d = {**item}
-            set_d.pop("id")
-            actions += update_entity(obj, info, type_node, set_d, {}, name_gen)
-            updated_objs += [obj]
-        else:
-            obj_name,more_actions = add_new_entity(info, type_node, item, name_gen)
-            actions += more_actions
-            new_obj_names += [obj_name]
+                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]
 
-    g = info.context["g"]
-    r = actions | transact[g] | run
+        r = commit_with_post_checks(actions, post_checks, info)
 
-    ents = updated_objs + [r[name] for name in new_obj_names]
-    count = len(ents)
+        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:
+        log.error("There was an error in resolve_upfetch", exc_info=exc)
+        raise Exception("Unexpected error")
             
         
 def resolve_update(_, info, *, type_node, **params):
-    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"])
+    # 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"])
 
-    actions = []
-    name_gen = NameGen()
-    for ent in ents:
-        actions += update_entity(ent, info, type_node, params["input"].get("set", {}), params["input"].get("remove", {}), name_gen)
+        actions = []
+        post_checks = []
 
-    g = info.context["g"]
-    r = actions | transact[g] | run
+        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
 
-    count = len(ents)
+        commit_with_post_checks(actions, post_checks, info)
 
-    # Note: we return the details after the update
-    ents = ents | map[now] | collect
+        count = len(ents)
 
-    return {"count": count, "ents": ents}
+        # Note: we return the details after the update
+        ents = ents | map[now] | collect
+
+        return {"count": count, "ents": ents}
+    except ExternalError:
+        raise
+    except Exception as exc:
+        log.error("There was an error in resolve_update", exc_info=exc)
+        raise Exception("Unexpected error")
 
 def resolve_delete(_, info, *, type_node, **params):
-    g = info.context["g"]
-    # Do the same thing as a resolve_query but delete the entities instead.
-    if "filter" not in params:
-        raise Exception("Not allowed to delete everything!")
-    ents = resolve_query(_, info, type_node=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)
 
-    if not ents | map[pass_delete_auth[type_node][info]] | all | collect:
-        raise Exception("Not allowed to delete")
+        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
+        [terminate[ent] for ent in ents] | transact[g] | run
 
-    count = len(ents)
+        count = len(ents)
 
-    return {"count": count, "ents": ents}
+        return {"count": count, "ents": ents}
+    except ExternalError:
+        raise
+    except Exception as exc:
+        log.error("There was an error in resolve_delete", exc_info=exc)
+        raise Exception("Unexpected error")
 
 def resolve_filter_response(obj, info, *, type_node, **params):
     ents = obj["ents"]
@@ -756,7 +886,7 @@ def NameGen():
         n += 1
         yield n
 
-def find_existing_entity(info, type_node, id):
+def find_existing_entity_by_id(info, type_node, id):
     if id is None:
         return None
     the_uid = uid(id)
@@ -765,21 +895,44 @@ def find_existing_entity(info, type_node, id):
     
     g = info.context["g"]
 
+    et = ET(type_node >> RT.GQL_Delegate | collect)
     ent = g[uid(id)] | now | collect
-    if not is_a(ent, ET(type_node >> RT.GQL_Delegate | collect)):
+    if not is_a(ent, et):
         return None
     if not ent | pass_query_auth[type_node][info] | collect:
         return None
 
     return ent
 
+def find_existing_entity_by_field(info, type_node, z_field, val):
+    if val is None:
+        raise Exception("Can't find an entity by a None field value")
+
+    g = info.context["g"]
+    et = ET(type_node >> 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
+    # might seem wrong, as it would try to create the same item again. However,
+    # because the field is also @unique then this will fail. Better to keep the
+    # 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]
+           | collect)
+
+    return ent
+
 def add_new_entity(info, type_node, params, name_gen):
-    if not pass_add_auth(type_node, info, params):
-        raise Exception("Not allowed to add.")
 
     actions = []
+    post_checks = []
 
     this = str(next(name_gen))
+    type_name = type_node >> RT.Name | value | collect
+
+    post_checks += [("add", this, type_node)]
 
     et = ET(type_node >> RT.GQL_Delegate | collect)
     actions += [et[this]]
@@ -787,18 +940,29 @@ def add_new_entity(info, type_node, params, 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
+        field_name = value(z_field >> RT.Name)
+        field_mapping[field_name] = z_field
+        # Convenient spot to validate that required fields have been specified.
+        if op_is_required(z_field):
+            if field_name not in params:
+                raise ExternalError(f"Required field '{field_name}' not given when creating new entity of type '{type_name}'")
+
 
-    # TODO: Validate that all required fields are present
     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)
         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
+                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.")
             if z_field | op_is_list | collect:
                 l = val
             else:
@@ -819,34 +983,41 @@ def add_new_entity(info, type_node, params, name_gen):
                 l = [val]
 
             for item in l:
-                obj,obj_actions = find_or_add_entity(item, info, target(z_field), params, name_gen)
+                obj,obj_actions,obj_post_checks = find_or_add_entity(item, info, target(z_field), params, name_gen)
                 actions += obj_actions
+                post_checks += obj_post_checks
                 if z_field | op_is_incoming | collect:
                     actions += [(obj, rt, Z[this])]
                 else:
                     actions += [(Z[this], rt, obj)]
 
-    return this, actions
+    return this, actions, post_checks
 
 def find_or_add_entity(val, info, target_node, params, name_gen):
     if isinstance(val, dict) and val.get("id", None) is not None:
-        obj = find_existing_entity(info, target_node, val["id"])
-        return obj,[]
+        obj = find_existing_entity_by_id(info, target_node, val["id"])
+        return obj,[],[]
     else:
-        obj_name,actions = add_new_entity(info, target_node, val, name_gen)
-        return Z[obj_name], actions
+        obj_name,actions,post_checks = add_new_entity(info, target_node, val, name_gen)
+        return Z[obj_name], actions, post_checks
     
 
 def update_entity(z, info, type_node, set_d, remove_d, name_gen):
-    # Refuse to set and remove the same thing. Just too confusing to deal with
-    if not pass_update_auth(z, type_node, info, set_d, remove_d):
-        raise Exception("Not allowed to add.")
+    type_name = type_node >> RT.Name | value | 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"
     if len(set_d) + len(remove_d) == 0:
-        raise Exception("No set or remove in update_entity!")
+        raise ExternalError("No set or remove in update_entity!")
+
+    # This is the pre-update auth only
+    # TODO: Post-update auth
+    if not pass_pre_update_auth(z, type_node, info):
+        raise ExternalError("Not allowed to update")
 
     actions = []
+    post_checks = []
+    post_checks += [("update", z, type_node)]
 
     # This should probably be cached
     field_mapping = {}
@@ -856,12 +1027,21 @@ def update_entity(z, info, type_node, set_d, remove_d, name_gen):
     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)
         # TODO: This should be able to distinguish based on the triple, not just the RT
         rt = RT(z_field >> RT.GQL_Resolve_With | collect)
 
         if op_is_list(z_field):
             raise Exception(f"Updating list things is a todo (for {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
+                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
             else:
@@ -878,9 +1058,12 @@ def update_entity(z, info, type_node, set_d, remove_d, name_gen):
                 raise Exception("Updating non-scalars is TODO")
     
     for key,val in remove_d.items():
-        # TODO: Validate that required fields are not removed
-        assert val is None, "Remove vals need to be nil"
+        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)
+        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)
         if op_is_incoming(z_field):
@@ -893,64 +1076,49 @@ def update_entity(z, info, type_node, set_d, remove_d, name_gen):
         if z_field | target | op_is_scalar | collect:
             actions += [terminate[target(rel)] for rel in rels]
 
-    return actions
+    return actions, post_checks
 
 
 ##############################
 # * Auth things
 #----------------------------
 
-@func
-def pass_query_auth(z, schema_node, info):
-    # TODO: Later on these will be zef functions so easier to call
-    if not schema_node | has_out[RT.AllowQuery] | collect:
+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
+        if to_call is not None:
+            break
+    else:
         return True
+
+    # TODO: Later on these will be zef functions so easier to call
     return temporary__call_string_as_func(
-        schema_node >> RT.AllowQuery | value | collect,
+        to_call | value | collect,
         z=z,
         info=info,
         type_node=schema_node
     )
 
 @func
-def pass_add_auth(schema_node, info, params):
-    # TODO: Later on these will be zef functions so easier to call
-    if not schema_node | has_out[RT.AllowAdd]| collect:
-        return True
-    return temporary__call_string_as_func(
-        schema_node >> RT.AllowAdd | value | collect,
-        info=info,
-        type_node=schema_node,
-        params=params
-    )
+def pass_query_auth(z, schema_node, info):
+    return pass_auth_generic(z, schema_node, info, [RT.AllowQuery])
 
 @func
-def pass_update_auth(z, schema_node, info, set_d, remove_d):
-    # TODO: Later on these will be zef functions so easier to call
-    if not schema_node | has_out[RT.AllowUpdate] | collect:
-        return (pass_query_auth(z, schema_node, info)
-                # TODO: This second part has to be handled properly through a PostCondition
-                #and pass_add_auth(schema_node, info))
-                )
-    return temporary__call_string_as_func(
-        schema_node >> RT.AllowUpdate | value | collect,
-        z=z,
-        info=info,
-        type_node=schema_node
-    )
+def pass_add_auth(z, schema_node, info):
+    return pass_auth_generic(z, schema_node, info, [RT.AllowAdd, RT.AllowQuery])
 
 @func
-def pass_delete_auth(z, schema_node, info):
-    # TODO: Later on these will be zef functions so easier to call
-    if not schema_node | has_out[RT.AllowDelete]| collect:
-        return pass_query_auth(z, schema_node, info)
-    return temporary__call_string_as_func(
-        root >> RT.AllowDelete | value | collect,
-        z=z,
-        info=info,
-        type_node=schema_node
-    )
+def pass_pre_update_auth(z, schema_node, info):
+    return pass_auth_generic(z, schema_node, info, [RT.AllowUpdate, RT.AllowQuery])
 
+@func
+def pass_post_update_auth(z, schema_node, info):
+    return pass_auth_generic(z, schema_node, info, [RT.AllowUpdatePost, RT.AllowUpdate, RT.AllowQuery])
+
+@func
+def pass_delete_auth(z, schema_node, info):
+    return pass_auth_generic(z, schema_node, info, [RT.AllowDelete, RT.AllowUpdate, RT.AllowQuery])
 
 def temporary__call_string_as_func(s, **kwds):
     from ... import core, ops
@@ -959,11 +1127,11 @@ def temporary__call_string_as_func(s, **kwds):
             s, {
                 **{name: getattr(core,name) for name in dir(core) if not name.startswith("_")},
                 **{name: getattr(ops,name) for name in dir(ops) if not name.startswith("_")},
+                "auth_field": P(auth_helper_auth_field, **kwds),
             },
             kwds
         )
     except Exception as exc:
-        from ...core.logger import log
         log.error("Problem calling auth expression", expr=s, exc_info=exc, kwds=kwds)
         return False
 
@@ -972,4 +1140,63 @@ def temporary__call_string_as_func(s, **kwds):
     elif isinstance(out, ZefOp):
         out = out(kwds.get("z", None))
 
-    return out
\ No newline at end of file
+    return out
+
+def auth_helper_auth_field(field_name, auth, *, z, type_node, info):
+    # A helper function for graphql schema, that requests an auth check of the
+    # given kind on one of its fields.
+
+    # Only makes sense for fields that are required, such as a user field.
+    try:
+        z_field = get_field_rel_by_name(type_node, field_name)
+        z_field_node = target(z_field)
+        val = field_resolver_by_name(z, type_node, info, field_name)
+    except:
+        # Going to assume this is because traversal failed auth along the way somewhere.
+        log.error("auth_field helper got an exception, assuming failure of auth")
+        return False
+
+    if auth == "query":
+        func = pass_query_auth
+    elif auth == "add":
+        func = pass_add_auth
+    elif auth == "update":
+        func = pass_pre_update_auth
+    elif auth == "updatePost":
+        func = pass_post_update_auth
+    elif auth == "delete":
+        func = pass_delete_auth
+    else:
+        raise Exception(f"Don't understand auth type '{auth}' in auth_helper_auth_field")
+
+    return func(val, z_field_node, info)
+
+
+
+def commit_with_post_checks(actions, post_checks, info):
+    g = info.context["g"]
+    with Transaction(g):
+        try:
+            r = actions | transact[g] | run
+            # Test all post checks
+            for (kind,obj,type_node) in post_checks:
+                if type(obj) == str:
+                    obj = r[obj]
+                assert type(obj) == ZefRef
+                obj = now(obj)
+                type_name = {type_node >> O[RT.Name] | value_or[''] | collect}
+
+                if kind == "add":
+                    if not pass_add_auth(obj, type_node, info):
+                        raise Exception(f"Add auth check for type_node of '{type_name}' returned False")
+                elif kind == "update":
+                    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")
+        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")
+
+    return r
diff --git a/python/zef/gql/simplegql/schema_file_parser.py b/python/zef/gql/simplegql/schema_file_parser.py
index ad1b44e8..d73d114c 100644
--- a/python/zef/gql/simplegql/schema_file_parser.py
+++ b/python/zef/gql/simplegql/schema_file_parser.py
@@ -15,6 +15,7 @@
 from ... import *
 from ...ops import *
 
+import json
 import graphql
 def parse_partial_graphql(schema):
     """Goes through the schema line by line and creates a json file that represents
@@ -23,30 +24,57 @@ def parse_partial_graphql(schema):
     """
     doc = graphql.parse(schema)
 
-    json = {
+    output = {
         "types": {},
         "enums": {}
     }
 
+    
+    # Find all Zef directives first, which are single-line comments that begin with # Zef.<name>:
+    lines = (schema 
+             | split["\n"] 
+             | filter[lambda x: x.startswith("# Zef.")]
+             | map[lambda x: x[len("# Zef."):]]
+             | collect)
+
+    for line in lines:
+        parts = line.split(':', 1)
+        assert len(parts) == 2, "Zef directive line contains no ':'."
+        name,details = parts
+        name = name.strip()
+
+        if name == "SchemaVersion":
+            assert "schema_version" not in output, "Not allowed to have multiple Zef.SchemaVersion directives."
+            output["schema_version"] = details.strip()
+            continue
+
+
+        # Anything else requires json parsing
+        try:
+            details = json.loads(details)
+        except Exception as exc:
+            raise Exception(f"JSON parsing of Zef directive '{name}' failed") from exc
+
+        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}'"
+            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.")
+
     for definition in doc.definitions:
         if definition.kind == "object_type_definition":
-            if definition.name.value == "_Auth":
-                # This is the special node to grab the auth details
-                assert len(definition.directives) == 1, "The _Auth type should have only a @details directive."
-                assert definition.directives[0].name.value == "details", "The _Auth type should have only a @details directive."
-                args = parse_arguments_as_dict(definition.directives[0].arguments)
-                json["auth"] = {}
-                for key,val in args.items():
-                    if key not in ["JWKURL", "Audience", "Header", "Namespace"]:
-                        raise Exception("Don't understand key of {key} in auth details.")
-                    json["auth"][key] = val
-                continue
-
-            # Otherwise this is a normal type
-            t_def = json["types"][definition.name.value] = {}
+            t_def = output["types"][definition.name.value] = {}
 
             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.")
                     for arg in directive.arguments:
                         if arg.name.value == "query":
                             t_def["_AllowQuery"] = arg.value.value
@@ -54,10 +82,24 @@ def parse_partial_graphql(schema):
                             t_def["_AllowAdd"] = arg.value.value
                         elif arg.name.value == "update":
                             t_def["_AllowUpdate"] = arg.value.value
+                        elif arg.name.value == "updatePost":
+                            t_def["_AllowUpdatePost"] = arg.value.value
                         elif arg.name.value == "delete":
                             t_def["_AllowDelete"] = arg.value.value
                         else:
                             raise Exception(f"Don't know how to handle auth kind of {arg.name.value}")
+                elif directive.name.value == "upfetch":
+                    assert len(directive.arguments) == 1, "upfetch directive needs exactly one argument, 'field'"
+                    arg = directive.arguments[0]
+                    if arg.name.value != "field":
+                        raise Exception("upfetch directive needs exactly one argument, 'field'")
+                    t_def["_Upfetch"] = arg.value.value
+                elif directive.name.value == "ET":
+                    assert len(directive.arguments) == 1, "ET directive needs exactly one argument, 'et'"
+                    arg = directive.arguments[0]
+                    if arg.name.value != "et":
+                        raise Exception("ET directive needs exactly one argument, 'et'")
+                    t_def["_ET"] = arg.value.value
                 else:
                     raise Exception(f"Don't know how to handle type directive @{directive.name.value}")
 
@@ -98,7 +140,7 @@ def parse_partial_graphql(schema):
                         else:
                             raise Exception(f"Don't know how to handle directive @{v}")
         elif definition.kind == "enum_type_definition":
-            e_def = json["enums"][definition.name.value] = []
+            e_def = output["enums"][definition.name.value] = []
             assert len(definition.directives) == 0, "Don't accept directives on enums"
 
             for value in definition.values:
@@ -108,7 +150,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 json
+
+
+    return output
 
 def string_to_RAET(s):
     if s.startswith("ET."):
@@ -129,6 +173,12 @@ def parse_arguments_as_dict(args):
     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):
     """This takes a json schema, such as one produced by the `parse_schema`
@@ -151,12 +201,38 @@ def json_to_minimal_nodes(json):
     # The root node meta data
     actions += [ET.GQL_Root["root"]]
     if "auth" in json:
+        # A bit of sanity checking here - we should have one algo, which selects
+        # symmetric/asymmetric keys, which allows only particular details to be
+        # given.
+        for key in ["Algo", "Header", "Audience"]:
+            assert key in json["auth"], f"Auth requires key '{key}'"
+        algo = json["auth"]["Algo"]
+        assert algo in ["RS256", "HS256"], f"Auth algo is '{algo}', must be one of 'RS256' or 'HS256'"
+
+        actions += [(Z["root"], RT.AuthAlgo, algo)]
         actions += [(Z["root"], RT.AuthHeader, json["auth"]["Header"])]
-        actions += [(Z["root"], RT.AuthJWKURL, json["auth"]["JWKURL"])]
         actions += [(Z["root"], RT.AuthAudience, json["auth"]["Audience"])]
+        actions += [(Z["root"], RT.AuthPublic, json["auth"].get("Public", True))]
         if "Namespace" in json["auth"]:
             actions += [(Z["root"], RT.AuthNamespace, json["auth"]["Namespace"])]
 
+        assert sum(x in json["auth"] for x in ["JWKURL", "VerificationKey", "VerificationKeyEnv"]) == 1, "Need exactly one of ['JWKURL', 'VerificationKey', 'VerificationKeyEnv'] in auth"
+        if algo == "RS256":
+            assert "JWKURL" in json["auth"], "RS256 needs JWKURL specified."
+            actions += [(Z["root"], RT.AuthJWKURL, json["auth"]["JWKURL"])]
+        if algo == "HS256":
+            vkey = json["auth"].get("VerificationKey", None)
+            if vkey is None:
+                env = json["auth"].get("VerificationKeyEnv", None)
+                if env is None:
+                    raise Exception("HS256 needs either VerificationKey or VerificationKeyEnv specified.")
+                import os
+                if env not in os.environ:
+                    raise Exception(f"VerificationKeyEnv asked for environment variable '{env}' which was not present.")
+                vkey = os.environ[env]
+            vkey = vkey.replace("\\n", '\n')
+            actions += [(Z["root"], RT.AuthPresharedKey, vkey)]
+
     for gql_name,typ in core_types.items():
         actions += [
             getattr(AET, typ)[gql_name],
@@ -187,10 +263,16 @@ def name_to_raet(name):
                 # Special handling here
                 if field_name == "_ET":
                     continue
-                
-                assert field_name in ["_AllowQuery", "_AllowAdd", "_AllowUpdate", "_AllowDelete"]
-                # TODO: Turn into a zef function later on
-                actions += [(Z[type_name], RT(field_name[1:]), field),]
+                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),]
+                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),]
+                else:
+                    raise Exception(f"Don't understand special field name '{field_name}'")
             else:
                 field = {**field}
 
@@ -201,7 +283,7 @@ def name_to_raet(name):
                 ]
                 for bool_key in ["search", "unique", "incoming", "list", "required"]:
                     if bool_key in field:
-                        actions += [(Z[qual_name], RT(to_pascal_case(bool_key)), field[bool_key])]
+                        actions += [(Z[qual_name], RT(simple_capitalize(bool_key)), field[bool_key])]
                         del field[bool_key]
 
                 if "resolver" in field:
@@ -213,7 +295,7 @@ def name_to_raet(name):
                         del field["relation"]
                     else:
                         this = ET(type_name)
-                        rt = RT(to_pascal_case(field_name))
+                        rt = RT(simple_capitalize(field_name))
                         other = name_to_raet(field["type"])
                         if field.get("incoming", False):
                             resolve_with = delegate_of((other, rt, this))
@@ -232,6 +314,4 @@ def name_to_raet(name):
         for opt in opts:
             actions += [(Z[enum_name], RT.GQL_Field, EN(enum_name, opt))]
 
-    # TODO: I want to validate that no inverse relation has not matched up correctly.
-
     return actions
diff --git a/python/zef/gql/simplegql/server.py b/python/zef/gql/simplegql/server.py
index e1e67a00..e6bf4548 100644
--- a/python/zef/gql/simplegql/server.py
+++ b/python/zef/gql/simplegql/server.py
@@ -25,8 +25,15 @@
 
 from zef.core.logger import log
 
-def resolve_token(request, header, aud, namespace):
-    from authlib.jose import jwt
+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
+
+    import jwt
     import base64
     headers_as_lower = {k.lower(): v for k,v in request["request_headers"].items()}
     if header.lower() not in headers_as_lower:
@@ -39,15 +46,21 @@ def resolve_token(request, header, aud, namespace):
     if len(parts) != 2 or parts[0] != "Bearer":
         # raise Exception(f"x-auth-header is of the wrong format ({token_header})")
         log.error("x-auth-header is of the wrong format", token_header=token_header)
-        return {
-            **request,
-            "response_body": "Invalid auth header",
-            "response_status": 400
-        }
+        raise Exception("Invalid auth header")
     token = parts[1]
 
-    auth_result = jwt.decode(token, context["jwk"])
-    auth_result.validate()
+    if algo == "RS256":
+        signing_key = context["jwk_client"].get_signing_key_from_jwt(token)
+        auth_result = jwt.decode(token,
+                                 signing_key.key,
+                                 algorithms=[algo],
+                                 audience=aud)
+    elif algo == "HS256":
+        psk = root >> RT.AuthPresharedKey | value | collect
+        auth_result = jwt.decode(token, psk, algorithms=[algo],
+                                 audience=aud)
+    else:
+        raise Exception("Shouldn't get here")
 
     if auth_result["aud"] != aud:
         raise Exception("Invalid token for wrong audience")
@@ -58,15 +71,30 @@ def resolve_token(request, header, aud, namespace):
         return auth_result[namespace]
             
 def query(request, context):
-    if context["z_gql_root"] | has_out[RT.AuthHeader] | collect:
-        header = context["z_gql_root"] >> RT.AuthHeader | value | collect
-        aud = context["z_gql_root"] >> RT.AuthAudience | value | collect
-        namespace = context["z_gql_root"] >> O[RT.AuthNamespace] | value_or[None] | collect
-
-        auth_context = resolve_token(request, header, aud, namespace)
+    root = context["z_gql_root"]
+    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):
+                raise Exception("No auth and public is False.")
+        except Exception as exc:
+            log.error("Auth failed", exc_info=exc)
+            return {
+                "response_body": "Auth failed",
+                "response_status": 400,
+                **request
+            }
     else:
         auth_context = None
 
+    if request["method"] == "GET" or request["request_body"].strip() == "":
+        # This is to give some kind of response instead of a failure. Helps with
+        # health checks.
+        return {
+            "response_body": "Server waiting for GraphQL requests",
+            **request,
+        }
+
     q = json.loads(request["request_body"])
 
     success,data = graphql_sync(
@@ -75,6 +103,8 @@ def query(request, context):
         context_value={"g": context["g_data"],
                        "auth": auth_context},
     )
+    if not success or "errors" in data:
+        log.error("Failure in GQL query.", data=data, q=q, auth_context=auth_context)
     return {
         "response_body": json.dumps(data),
         **request
@@ -89,30 +119,23 @@ def start_server(z_gql_root,
     schema,objects = generate_resolvers_fcts(z_gql_root)
     ari_schema = make_executable_schema(schema, objects)
 
-    if z_gql_root | has_out[RT.AuthHeader] | collect:
-        url = z_gql_root >> RT.AuthJWKURL | value | collect
-        # import urllib
-        # with urllib.request.urlopen(url) as response:
-        #     raw = response.read()
-        #     # max_age = parse_max_age(response.headers)
-        #     # authority_refresh.on_next(max_age)
-        # import json
-        # app.jwk = json.loads(raw)
-
-        import requests
-        r = requests.get(url)
-        jwk = r.json()
-    else:
-        jwk = None
-
-
+    import logging
+    logger = logging.getLogger("ariadne").disabled = True
+    log.debug("Disabled ariadne logging")
+    
     context = {
         "z_gql_root": z_gql_root,
         "g_data": g_data,
         "ari_schema": ari_schema,
-        "jwk": jwk,
     }
 
+    if z_gql_root | has_out[RT.AuthJWKURL] | collect:
+        url = z_gql_root >> RT.AuthJWKURL | value | collect
+
+        from jwt import PyJWKClient
+        context["jwk_client"] = PyJWKClient(url)
+
+
     http_r = Effect({
         'type': FX.HTTP.StartServer,
         'port': port,


Test patch:
diff --git a/core/scripts/Dockerfile.build_test b/core/scripts/Dockerfile.build_test
deleted file mode 100644
index 5f16534a..00000000
--- a/core/scripts/Dockerfile.build_test
+++ /dev/null
@@ -1,26 +0,0 @@
-FROM pengwyn/julia-python-base:jl1.6.0-py3.8-v1.7
-
-# ARG CODEARTIFACT_TOKEN
-# ARG GIT_CREDENTIALS
-ARG ZEFHUB_AUTH_KEY
-
-# ARG VERSION_STRING
-# ARG WITH_JULIA
-
-RUN echo "Building with version string $VERSION_STRING and WITH_JULIA=$WITH_JULIA"
-
-RUN apt-get update
-RUN apt-get install -y openssl libssl-dev zstd libzstd-dev libsecret-1-dev
-
-RUN pip3 install absl-py protobuf
-
-COPY . .
-
-# RUN jlpkg registry add https://github.com/synchronoustechnologies/JuliaRegistry
-
-RUN bash make_cleanslate.sh
-RUN bash make_everything.sh
-
-# RUN bash setup_tools_wrapper.sh
-
-# RUN twine upload -r codeartifact dist/*
diff --git a/python/tests/quick_start.py b/python/tests/quick_start.py
new file mode 100644
index 00000000..c4dc99fc
--- /dev/null
+++ b/python/tests/quick_start.py
@@ -0,0 +1,26 @@
+# 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.
+
+# This test is meant to check whether zef shuts down properly when it isn't given time to connect to ZefHub
+import os
+os.environ["ZEFHUB_AUTH_KEY"] = "GUEST"
+# These shouldn't be necessary but can't hurt, prevents overrides from the CI
+# environment taking over..
+os.environ["ZEFDB_LOGIN_AUTOCONNECT"] = "TRUE"
+os.environ["ZEFDB_BUTLER_AUTOSTART"] = "TRUE"
+
+from zef import *
+from zef.ops import *
+import zef
+
diff --git a/python/tests/rem_test_http.py b/python/tests/rem_test_http.py
new file mode 100644
index 00000000..f3f39c76
--- /dev/null
+++ b/python/tests/rem_test_http.py
@@ -0,0 +1,52 @@
+# 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 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({
+            'type': FX.HTTP.StartServer,
+            # We hope that this is not taken by anything else for this test...
+            'port': port,
+            'pipe_into': (map[middleware_worker[permit_cors,
+                                                route["/test-url"][lambda query: {**query, "response_body": payload}],
+                                                fallback_not_found,
+                                                send_response]]
+                          | subscribe[run]),
+            'bind_address': "localhost",
+        })
+
+        eff_resp = eff | run
+
+        import requests
+        r = requests.get(f"http://localhost:{port}/test-url")
+        self.assertEqual(r.status_code, 200)
+        self.assertEqual(r.content, payload)
+
+        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
+
+if __name__ == '__main__':
+    unittest.main()
diff --git a/python/tests/requirements_tests.txt b/python/tests/requirements_tests.txt
index b676d56b..bebde7df 100644
--- a/python/tests/requirements_tests.txt
+++ b/python/tests/requirements_tests.txt
@@ -1,2 +1,3 @@
 pytest
-networkx>=2.6.3
\ No newline at end of file
+networkx>=2.6.3
+requests
diff --git a/python/tests/test_graph_delta.py b/python/tests/test_graph_delta.py
index 9a0f71aa..acf31a96 100644
--- a/python/tests/test_graph_delta.py
+++ b/python/tests/test_graph_delta.py
@@ -138,5 +138,18 @@ def test_relation_relations(self):
         x, y, z = (y, RT.Something, 3) | g | run
         g | now | all[ET.Example] | last | out_rel[RT.Something] | Out[RT.Something] | collect
 
+    def test_tagging(self):
+        g = Graph()
+
+        z = ET.Person | g | run
+        r = [
+            ET.Person["joe"],
+            Z["joe"] | tag["secret"],
+            z | tag["first"]
+        ] | transact[g] | run
+
+        self.assertEqual(r["joe"], g | now | get["secret"] | collect)
+        self.assertEqual(z | now | collect, g | now | get["first"] | collect)
+
 if __name__ == '__main__':
     unittest.main()
diff --git a/python/tests/test_tx_linking.py b/python/tests/test_tx_linking.py
new file mode 100644
index 00000000..80bf658d
--- /dev/null
+++ b/python/tests/test_tx_linking.py
@@ -0,0 +1,77 @@
+# 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.
+
+# This file contains tests for things that were bugs at one point. Does not need
+# to be organised at all
+
+import unittest  # pytest takes ages to run anything as soon as anything from zef is imported
+import os
+from zef import *
+from zef.ops import *
+import zef
+
+
+class MyTestCase(unittest.TestCase):
+
+    def test_linking_tx(self):
+        g = Graph()
+
+        # Get the first tx on the graph
+        tx_node = g | all[TX] | first | collect
+
+        r = [
+            (tx_node, RT.User, "someone"),
+        ] | transact[g] | run
+
+        self.assertEqual("someone", tx_node | now | Out[RT.User] | value | collect)
+
+        r = [
+            (tx_node, RT.Something, g | root | collect),
+        ] | transact[g] | run
+
+        # Note: the root has delegates coming off of it, so we need to filter by only the concrete relation.
+        self.assertEqual(g | now | root | in_rels[RT.Something] | filter[Not[is_a[Delegate]]] | single | collect,
+                         tx_node | now | out_rel[RT.Something] | collect)
+
+    def test_linking_with_transaction(self):
+        g = Graph()
+
+        with Transaction(g) as ctx:
+            (ctx, RT.User, "Someone") | g | run
+
+        self.assertEqual(g | all[TX] | last | now | Out[RT.User] | value | collect,
+                         "Someone")
+
+    def test_attaching_to_delegates(self):
+        g = Graph()
+
+        with Transaction(g) as ctx:
+            (ctx, RT.User, "Someone") | g | run
+
+        (delegate_of(ctx), RT.Fixed, True) | g | run
+        # Note: True so that we create the delegate
+        (g | root | delegate_of[True] | collect, RT.Root, True) | g | run
+        # Note: trying alternative with abstract delegate
+        (g | root | to_delegate | delegate_of | collect, RT.Second, True) | g | run
+
+    def test_failure_on_other_graph(self):
+        g = Graph()
+        g2 = Graph()
+
+        tx_node = g | all[TX] | first | collect
+        with self.assertRaisesRegex(Exception, "Can only merge TXNode and Root onto the same graph"):
+            (tx_node, RT.User, "someone") | g2 | run
+
+if __name__ == '__main__':
+    unittest.main()