diff --git a/setup.py b/setup.py
index 3ad381ac0a111e758849cbc05e2ad3a9819e4b8a..16eb60ef0c25370658d8bc8270db0a40c8618789 100644
--- a/setup.py
+++ b/setup.py
@@ -5,7 +5,7 @@ from subprocess import check_output
 import numpy
 from Cython.Build import cythonize
 from Cython.Distutils import build_ext
-from setuptools import setup, find_packages
+from setuptools import find_packages, setup
 
 from src.xfel_calibrate.notebooks import notebooks
 
@@ -36,10 +36,12 @@ class PreInstallCommand(build):
 data_files = []
 for ctypes in notebooks.values():
     for nb in ctypes.values():
-        data_files.append(nb["notebook"])
+        data_files.append(nb.get("notebook"))
         data_files += nb.get("dep_notebooks", [])
         data_files += nb.get("pre_notebooks", [])
 
+data_files = list(filter(None, data_files))  # Get rid of `None` entries
+
 setup(
     name="European XFEL Offline Calibration",
     version="1.0",
diff --git a/src/xfel_calibrate/calibrate.py b/src/xfel_calibrate/calibrate.py
index 951dba8ea94b711cff5ce1796b7486633696f107..79ecf612087d5cdc16231d9d1e9e2a375c9f38f9 100755
--- a/src/xfel_calibrate/calibrate.py
+++ b/src/xfel_calibrate/calibrate.py
@@ -1,6 +1,7 @@
 #!/usr/bin/env python
 
 import argparse
+import ast
 import inspect
 import math
 import os
@@ -13,7 +14,7 @@ import warnings
 from datetime import datetime
 from pathlib import Path
 from subprocess import DEVNULL, check_output
-from typing import List, Union
+from typing import List, Optional, Union
 
 import nbformat
 import numpy as np
@@ -359,9 +360,25 @@ def make_extended_parser() -> argparse.ArgumentParser:
         # The information is extracted from the first markdown cell of
         # the notebook.
         for caltype, notebook in det_notebooks.items():
-            nbpath = os.path.join(PKG_DIR, notebook["notebook"])
-            nb = nbformat.read(nbpath, as_version=4)
-            msg += make_epilog(nb, caltype=caltype)
+            if notebook.get("notebook") is None:
+                if notebook.get("user") is None:
+                    raise KeyError(
+                        f"`{detector}` does not have a notebook path, for "
+                        "notebooks that are stored in pycalibration set the "
+                        "`notebook` key to a relative path or set the "
+                        "`['user']['notebook']` key to an absolute path/path "
+                        "pattern. Notebook configuration dictionary contains "
+                        f"only: `{notebook}`"
+                        ""
+                    )
+                #  Everything should be indented by 17 spaces
+                msg += caltype.ljust(17) + "User defined notebook, arguments may vary\n"
+                msg += " "*17 + "User notebook expected to be at path:\n"
+                msg += " "*17 + notebook["user"]["notebook"] + "\n"
+            else:
+                nbpath = os.path.join(PKG_DIR, notebook["notebook"])
+                nb = nbformat.read(nbpath, as_version=4)
+                msg += make_epilog(nb, caltype=caltype)
 
         return make_initial_parser(epilog=msg)
     elif len(sys.argv) <= 3:
@@ -376,12 +393,37 @@ def make_extended_parser() -> argparse.ArgumentParser:
         print("Not one of the known calibrations or detectors")
         sys.exit(1)
 
-    notebook = os.path.join(PKG_DIR, nb_info["notebook"])
-    cvar = nb_info.get("concurrency", {}).get("parameter", None)
+    if nb_info["notebook"]:
+        notebook = os.path.join(PKG_DIR, nb_info["notebook"])
+    else:
+        # If `"notebook"` entry is None, then set it to the user provided
+        # notebook TODO: This is a very hacky workaround, better implementation
+        # is not really possible with the current state of this module
+        user_notebook_path = nb_info["user"]["notebook"]
+        #  Pulls out the variables in the templated path string, so that they
+        #  can be added to the argument parser
+        user_notebook_variables = [
+            k.value.id
+            for k
+            in ast.walk(ast.parse(f"f'{user_notebook_path}'"))
+            if isinstance(k, ast.FormattedValue)
+        ]
+
+        user_notebook_parser = argparse.ArgumentParser()
+
+        for var in user_notebook_variables:
+            user_notebook_parser.add_argument(f"--{var}")
+
+        user_notebook_args, _ = user_notebook_parser.parse_known_args(
+            args=list(filter(lambda x: x != "-h", _))  # Drop help from args
+        )
 
-    nb = nbformat.read(notebook, as_version=4)
+        nb_info["notebook"] = nb_info["user"]["notebook"].format(**vars(user_notebook_args))
+        notebook = str(nb_info["notebook"])
 
+    cvar = nb_info.get("concurrency", {}).get("parameter", None)
 
+    nb = nbformat.read(notebook, as_version=4)
 
     # extend parameters if needed
     ext_func = nb_info.get("extend parms", None)
@@ -663,10 +705,12 @@ def remove_duplications(l):
     return unique_l
 
 
-def concurrent_run(temp_path, nb, nbname, args, cparm=None, cval=None,
-                   final_job=False, job_list=[], fmt_args={}, cluster_cores=8,
-                   sequential=False, dep_jids=[],
-                   show_title=True):
+def concurrent_run(
+    temp_path, nb, nbname, args, cparm=None, cval=None,
+    final_job=False, job_list=[], fmt_args={}, cluster_cores=8,
+    sequential=False, dep_jids=[],
+    show_title=True, user_venv: Optional[Path] = None,
+):
     """ Launch a concurrent job on the cluster via SLURM
     """
 
@@ -707,16 +751,21 @@ def concurrent_run(temp_path, nb, nbname, args, cparm=None, cval=None,
         srun_base = get_launcher_command(args, temp_path, dep_jids)
         print(" ".join(srun_base))
 
-    srun_base += [os.path.join(PKG_DIR, "bin", "slurm_calibrate.sh"),  # path to helper sh
-                  os.path.abspath(nbpath),  # path to notebook
-                  python_path,  # path to python
-                  cluster_profile,
-                  '"{}"'.format(base_name.upper()),
-                  '"{}"'.format(args["detector"].upper()),
-                  '"{}"'.format(args["type"].upper()),
-                  "FINAL" if final_job else "NONFINAL",
-                  "{}/finalize.py".format(os.path.abspath(temp_path)),
-                  str(cluster_cores)]
+    if user_venv:
+        print(f"Running job in user venv at {user_venv}\n")
+
+    srun_base += [
+        os.path.join(PKG_DIR, "bin", "slurm_calibrate.sh"),  # path to helper sh
+        os.path.abspath(nbpath),  # path to notebook
+        user_venv + "/bin/python" if user_venv else python_path,  # path to python
+        cluster_profile,
+        '"{}"'.format(base_name.upper()),
+        '"{}"'.format(args["detector"].upper()),
+        '"{}"'.format(args["type"].upper()),
+        "FINAL" if final_job else "NONFINAL",
+        "{}/finalize.py".format(os.path.abspath(temp_path)),
+        str(cluster_cores)
+    ]
 
     output = check_output(srun_base).decode('utf8')
     jobid = None
@@ -924,6 +973,10 @@ def run():
                 'submission_time': submission_time
                 }
 
+    user_venv = None
+    if nb_info.get("user", {}).get("venv"):
+        user_venv = nb_info["user"]["venv"].format(**args)
+
     joblist = []
     cluster_cores = concurrency.get("cluster cores", 8)
     # Check if there are pre-notebooks
@@ -935,7 +988,7 @@ def run():
                                args,
                                job_list=joblist, fmt_args=fmt_args,
                                cluster_cores=cluster_cores,
-                               sequential=sequential,
+                               sequential=sequential, user_venv=user_venv
                               )
         joblist.append(jobid)
 
@@ -946,7 +999,7 @@ def run():
                                fmt_args=fmt_args,
                                cluster_cores=cluster_cores,
                                sequential=sequential,
-                               dep_jids=joblist,
+                               dep_jids=joblist, user_venv=user_venv
                               )
         joblist.append(jobid)
     else:
diff --git a/src/xfel_calibrate/notebooks.py b/src/xfel_calibrate/notebooks.py
index 1b4e4cd965172286162c83590c30175bf17041a8..d21d2d5cf5a4fe877e0d8e83298fdc7dc53e96dd 100644
--- a/src/xfel_calibrate/notebooks.py
+++ b/src/xfel_calibrate/notebooks.py
@@ -233,6 +233,21 @@ notebooks = {
                             "cluster cores": 16},
         },
     },
+    "REMI": {
+        "CORRECT": {
+            "notebook": None,
+            "user": {
+                "notebook": "/gpfs/exfel/exp/{instrument}/{cycle}/p{proposal}/usr/calibration/notebooks/correct.ipynb",
+                "venv":"/gpfs/exfel/exp/{instrument}/{cycle}/p{proposal}/usr/calibration/.venv"
+            },
+            "concurrency": {
+                "parameter": None,
+                "use function": None,
+                "default concurrency": None,
+                "cluster cores": 1
+            },
+        },
+    },
     "TEST": {
         "TEST-CLI": {
             "notebook": "notebooks/test/test-cli.ipynb",
@@ -242,5 +257,36 @@ notebooks = {
                 "cluster cores": 1,
             },
         },
+        "TEST-USER-NB": {
+            "notebook": None,
+            "user": {
+                "notebook": "/{root}/test-cli.ipynb",
+                "venv": None,  # default, pycalibration environment
+            },
+            "concurrency": {
+                "parameter": None,
+                "use function": None,
+                "default concurrency": None,
+                "cluster cores": 1
+            },
+        },
+        "TEST-USER-NB-VENV": {
+            "notebook": None,
+            "user": {
+                "notebook": "/{root}/test-cli.ipynb",
+                "venv": "/{root}/.venv",
+            },
+            "concurrency": {
+                "parameter": None,
+                "use function": None,
+                "default concurrency": None,
+                "cluster cores": 1
+            },
+        },
+    },
+    "TEST-RAISES-ERRORS": {
+        "TEST-BAD-KEY": {
+            "noteboke": "a typo",
+        }
     },
 }