diff --git a/apps/cli/executables/null/README.md b/apps/cli/executables/null/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..2840736025b0f9676f061d687d4c28451517db8c
--- /dev/null
+++ b/apps/cli/executables/null/README.md
@@ -0,0 +1,29 @@
+This is the null executable, a baseline test of the functionality of the Workspaces system.
+
+It can:
+- Print a message to stderr
+- Print a message to stdout
+- Exit with status code -1
+- Exit with random status code in [-50, 50]
+- Sleep for 5 seconds
+- Abort and dump the core
+ 
+```
+usage: null [-h] [-v] [-pe | -g | -ef | -er | -n | -d]
+
+Workspaces null executable, a status capture test of the system. Version 4.0.0a1.dev1
+
+optional arguments:
+  -h, --help          show this help message and exit
+  -pe, --print-error  print out aggressive message to stderr
+  -g, --greeting      print out a friendly greeting to stdout
+  -ef, --exit-fail    print error message and exit with status code -1
+  -er, --exit-random  print error message and exit with random status code within [-50, 50]
+  -n, --nap           take a short nap
+  -d, --dump          abort program and dump core
+
+options:
+  settings for altering program behavior
+
+  -v, --verbose       allow the program the gift of speech
+```
diff --git a/apps/cli/executables/null/__init__.py b/apps/cli/executables/null/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/apps/cli/executables/null/setup.py b/apps/cli/executables/null/setup.py
new file mode 100644
index 0000000000000000000000000000000000000000..91b6b575c50c3c08f10dbc16446d91301d71d40c
--- /dev/null
+++ b/apps/cli/executables/null/setup.py
@@ -0,0 +1,36 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+from pathlib import Path
+from setuptools import setup
+
+VERSION = open('src/null/_version.py').readlines()[-1].split()[-1].strip("\"'")
+README = Path('README.md').read_text()
+
+# requires = [
+# ]
+
+tests_require = [
+    'pytest>=5.4,<6.0'
+]
+setup(
+    name=Path().absolute().name,
+    version=VERSION,
+    description='Workspaces null executable.',
+    long_description=README,
+    author='NRAO SSA Team',
+    author_email='dms-ssa@nrao.edu',
+    url='TBD',
+    license="GPL",
+    # install_requires=requires,
+    tests_require=tests_require,
+    keywords=[],
+    packages=['null'],
+    package_dir={'':'src'},
+    classifiers=[
+        'Programming Language :: Python :: 3.8'
+    ],
+    entry_points={
+        'console_scripts': ['null = null.null:main']
+    },
+)
diff --git a/apps/cli/executables/null/src/null/__init__.py b/apps/cli/executables/null/src/null/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/apps/cli/executables/null/src/null/_version.py b/apps/cli/executables/null/src/null/_version.py
new file mode 100644
index 0000000000000000000000000000000000000000..f27d146a3f39885ce269bacf9ab4510254147c8d
--- /dev/null
+++ b/apps/cli/executables/null/src/null/_version.py
@@ -0,0 +1,2 @@
+""" Version information for this package, don't put anything else here. """
+___version___ = '4.0.0a1.dev1'
diff --git a/apps/cli/executables/null/src/null/null.py b/apps/cli/executables/null/src/null/null.py
new file mode 100644
index 0000000000000000000000000000000000000000..6f806ae60910206b36c8a97d3fb9eca700595529
--- /dev/null
+++ b/apps/cli/executables/null/src/null/null.py
@@ -0,0 +1,111 @@
+""" Module for the null executable. Performs some very basic actions
+    and utilizes pymygdala's LogHandler for logging. """
+
+import os
+import sys
+import time
+import random
+import logging
+import argparse
+
+from ._version import ___version___ as version
+
+_DESCRIPTION = """Workspaces null executable, a status capture test of the system. Version {}"""
+
+logger = logging.getLogger("null")
+logger.setLevel(logging.INFO)
+handler = logging.StreamHandler(stream=sys.stdout)
+
+class Null:
+    def __init__(self, args, verbose):
+        self.args = args
+        if verbose:
+            logger.setLevel(logging.DEBUG)
+        self.args_to_funcs = {
+            'print-error': self.print_error,
+            'greeting': self.print_greeting,
+            'exit-fail': self.exit_with_failure,
+            'exit-random': self.exit_randomly,
+            'nap': self.take_nap,
+            'dump': self.dump_core
+        }
+
+    def print_error(self):
+        logger.removeHandler(handler)
+        err_handler = logging.StreamHandler(stream=sys.stderr)
+        logger.addHandler(err_handler)
+        logger.error("ERROR: This is an error.")
+
+    def print_greeting(self):
+        logger.info("Hello, world!")
+        logger.debug("And goodbye, world...")
+
+    def exit_with_failure(self):
+        logger.error("Error purposefully induced. Exiting with status code -1...")
+        sys.exit(-1)
+
+    def exit_randomly(self):
+        status_code = random.randint(-50, 50)
+        logger.debug("Exiting with status code {}".format(status_code))
+        sys.exit(status_code)
+
+    def take_nap(self):
+        logger.debug("Going to sleep...")
+        time.sleep(5)
+        logger.debug("Waking up.")
+
+    def dump_core(self):
+        logger.debug("Aborting and dumping core...", stack_info=True)
+        os.abort()
+
+    def execute(self):
+        """
+        Executes command specified by CL arguments.
+        """
+        for arg, val in vars(self.args).items():
+            if val and arg in self.args_to_funcs:
+                self.args_to_funcs[arg]()
+
+def make_arg_parser():
+    """
+    Creates an argparse arguments parser with appropriate options
+    :return: Said argument parser
+    """
+    parser = argparse.ArgumentParser(description=_DESCRIPTION.format(version),
+                                     formatter_class=argparse.RawTextHelpFormatter)
+    options = parser.add_argument_group('options', 'settings for altering program behavior')
+    options.add_argument('-v', '--verbose', action='store_true',
+                         required=False, dest='verbose', default=False,
+                         help='allow the program the gift of speech')
+
+    functions = parser.add_mutually_exclusive_group(required=False)
+    functions.add_argument('-pe', '--print-error', action='store_true',
+                           required=False, dest='print-error', default=False,
+                           help='print out aggressive message to stderr')
+    functions.add_argument('-g', '--greeting', action='store_true',
+                           required=False, dest='greeting', default=False,
+                           help='print out a friendly greeting to stdout')
+    functions.add_argument('-ef', '--exit-fail', action='store_true',
+                           required=False, dest='exit-fail', default=False,
+                           help='print error message and exit with status code -1')
+    functions.add_argument('-er', '--exit-random', action='store_true',
+                           required=False, dest='exit-random', default=False,
+                           help='print error message and exit with random status code within [-50, 50]')
+    functions.add_argument('-n', '--nap', action='store_true',
+                           required=False, dest='nap', default=False,
+                           help='take a short nap')
+    functions.add_argument('-d', '--dump', action='store_true',
+                           required=False, dest='dump', default=False,
+                           help='abort program and dump core')
+    return parser
+
+def main():
+    arg_parser = make_arg_parser()
+    args = arg_parser.parse_args()
+    logger.addHandler(handler)
+
+    executable = Null(args, args.verbose)
+    executable.execute()
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file
diff --git a/apps/cli/executables/null/test/__init__.py b/apps/cli/executables/null/test/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/apps/cli/executables/null/test/test_null.py b/apps/cli/executables/null/test/test_null.py
new file mode 100644
index 0000000000000000000000000000000000000000..7dd49717c246daa13db90f959016aa4ce59af8dc
--- /dev/null
+++ b/apps/cli/executables/null/test/test_null.py
@@ -0,0 +1,36 @@
+import pytest
+import argparse
+
+# from null.null import Null
+from ..src.null.null import Null
+
+@pytest.fixture()
+def null():
+    null = Null(argparse.Namespace(), True)
+    return null
+
+def test_print_error(null, caplog):
+    null.print_error()
+    assert 'ERROR: This is an error.' in caplog.text
+
+def test_print_greeting(null, caplog):
+    null.print_greeting()
+    assert 'Hello, world!' in caplog.text
+    assert 'And goodbye, world...' in caplog.text
+
+def test_exit_with_failure(null, caplog):
+    with pytest.raises(SystemExit) as e:
+        null.exit_with_failure()
+        assert 'Error purposefully induced. Exiting with status code -1...' in caplog.text
+        assert e.value.code == -1
+
+def test_exit_randomly(null, caplog):
+    with pytest.raises(SystemExit) as e:
+        null.exit_randomly()
+        assert 'Exiting with status code' in caplog.text
+        assert -50 <= e.value.code <= 50
+
+def test_take_nap(null, caplog):
+    null.take_nap()
+    assert 'Going to sleep...' in caplog.text
+    assert 'Waking up.' in caplog.text
\ No newline at end of file
diff --git a/build/recipes/build_pkgs/build_pkgs.py b/build/recipes/build_pkgs/build_pkgs.py
index ad50462f74a3f8ed847c85e21b000e999551bdde..f542b7b3b63d112b31c109be6e3f02f4552439f3 100644
--- a/build/recipes/build_pkgs/build_pkgs.py
+++ b/build/recipes/build_pkgs/build_pkgs.py
@@ -1,16 +1,38 @@
 import subprocess
 
-def get_pkg_list():
+def get_dirs():
     """
-    Run a couple shell commands to parse the metadata directory for its packages.
-    :return: List of packages in metadata directory
+    Finds all subdirectories containing setup.py files.
+    :return: List of directories as strings.
     """
-    find_proc = subprocess.run(["find", "build/metadata",
-                                  "-name", "meta.yaml"],
-                                 stdout=subprocess.PIPE)
-    paths = find_proc.stdout.decode('utf-8')
-    fmt_paths = paths.replace("build/metadata/", "").replace("/meta.yaml", "")
-    return fmt_paths.split('\n')
+    find = subprocess.run([
+        'find', '.', '-name', 'setup.py', '-not', '-path', './build/recipes/*'
+    ], stdout=subprocess.PIPE)
+    dirs = find.stdout.decode('utf-8').split('\n')
+    dirs_cpy = dirs
+
+    for i, d in enumerate(dirs_cpy):
+        dirs[i] = d.replace('/setup.py', '')
+
+    return dirs
+
+def get_names(dirs):
+    """
+    Generate list of subproject names based on the rule that the name of the
+    subproject directory will be the name of the subproject.
+    :return: List of names as strings.
+    """
+    names = []
+    for d in dirs:
+        if d != '':
+            name = d.split('/')[-1]
+
+            if name == "archive":
+                # Case with ./services/archive having special dir structure
+                name = "services"
+            names.append(name)
+
+    return names
 
 class Recipe:
     def __init__(self, buildout, name, options):
@@ -23,7 +45,7 @@ class Recipe:
         """
         self.name = name
         self.options = options
-        self.pkg_list = get_pkg_list()
+        self.pkg_list = get_names(get_dirs())
 
     def install(self):
         """
diff --git a/build/recipes/setup_to_meta/setup_to_meta.py b/build/recipes/setup_to_meta/setup_to_meta.py
index e26e4777b24cf0c638dc4cdba01eeff83f3d9278..e65c1ba2ae9e104a35dfb543cc9985c0224ab107 100644
--- a/build/recipes/setup_to_meta/setup_to_meta.py
+++ b/build/recipes/setup_to_meta/setup_to_meta.py
@@ -46,18 +46,20 @@ class MetadataGenerator:
         """
         reqs_string = ''
         reqs_list = ''
+        reqs_string += 'requirements:\n'
+        build_reqs = '  build:\n'
+        run_reqs = '  run:\n'
+        host_reqs = '  host:\n'
+        reqs_list += '    - python={}\n'.format(PYTHON_VERSION)
+
         if 'install_requires' in self.setup.keys():
-            reqs_string += 'requirements:\n'
-            build_reqs = '  build:\n'
-            run_reqs = '  run:\n'
-            host_reqs = '  host:\n'
-            reqs_list += '    - python={}\n'.format(PYTHON_VERSION)
             for req in self.setup['install_requires']:
                 reqs_list += '    - {}\n'.format(req)
-            reqs_string += build_reqs + reqs_list + \
-                           run_reqs + reqs_list + \
-                           host_reqs + reqs_list + \
-                           '\n'
+
+        reqs_string += build_reqs + reqs_list + \
+                       run_reqs + reqs_list + \
+                       host_reqs + reqs_list + \
+                       '\n'
         return reqs_string
 
     def fmt_test(self):
@@ -88,8 +90,11 @@ class MetadataGenerator:
 
     def generate(self):
         # Filter numpy etc. out of the requirements
-        self.setup['install_requires'] = [req for req in self.setup['install_requires'] if req != 'numpy']
-    
+        try:
+            self.setup['install_requires'] = [req for req in self.setup['install_requires'] if req != 'numpy']
+        except KeyError:
+            pass
+
         name = self.setup['name']
         version = self.setup['version']
         entry_points = self.fmt_ep()
diff --git a/build/tools/transfer_to_builder.py b/build/tools/transfer_to_builder.py
index 6ae2edf2a24991e2aad023b7b65237f956d132eb..64d7e4fc7ff61c4fdc8b32d4f8c7fd627f7981d8 100644
--- a/build/tools/transfer_to_builder.py
+++ b/build/tools/transfer_to_builder.py
@@ -1,4 +1,9 @@
-import subprocess, paramiko, fnmatch, os
+import subprocess
+import paramiko
+import fnmatch
+import os
+import getpass
+
 from scp import SCPClient
 
 def get_build_pkg_names():
@@ -23,7 +28,11 @@ def create_ssh_client(server):
     client = paramiko.SSHClient()
     client.load_system_host_keys()
     client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
-    client.connect(server)
+
+    username = input("Enter NRAO username: ")
+    password = getpass.getpass(prompt="Enter NRAO password: ")
+
+    client.connect(server, username=username, password=password)
     return client
 
 def transfer_packages(pkg_names):
@@ -34,9 +43,9 @@ def transfer_packages(pkg_names):
     if len(pkg_names):
         builder_addr = "builder.aoc.nrao.edu"
         builder_path = "/home/builder.aoc.nrao.edu/content/conda/noarch"
-        ssh = create_ssh_client(builder_addr)
-        with SCPClient(ssh.get_transport()) as scp:
-            [scp.put(pkg, builder_path) for pkg in pkg_names]
+        with create_ssh_client(builder_addr) as ssh:
+            with SCPClient(ssh.get_transport()) as scp:
+                [scp.put(pkg, builder_path) for pkg in pkg_names]
         cmd_cd = "cd {}".format(builder_path)
         cmd_index = "conda index .."
         cmd_chmod = "chmod -f 664 *"
diff --git a/environment.yml b/environment.yml
index e42e0d028dc8b175016cda74043e2cd13651f303..f64d06a7133dbcea3db8ec4b3610aaeffb882891 100644
--- a/environment.yml
+++ b/environment.yml
@@ -13,6 +13,7 @@ dependencies:
   - jxmlease=1.0
   - lxml=4.5
   - mysqlclient=1.4
+  - paramiko
   - pandas=1.0
   - pendulum=2.1
   - pid=2.2