diff --git a/apps/cli/executables/null/README.md b/apps/cli/executables/null/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..b2cc894843867d2fa2ab8c1a76de9c55036411ac
--- /dev/null
+++ b/apps/cli/executables/null/README.md
@@ -0,0 +1,41 @@
+
+# datafetcher: data-fetcher for the AAT/PPI
+
+This is an implementation of the NRAO archive's Java-based data-fetcher in Python, as a research experiment on how it could be modularized and re-structured.
+ 
+Use cases datafetcher is intended to handle:
+   * works for both science products and ancillary products
+   * download a product from the NRAO archive by specifying its product locator
+   * download a product from the NRAO archive by providing a path to a locator report
+   * streaming or direct copy downloads based on file location and execution site
+   
+This is intended to be a library wrapped in a command line interface.
+ 
+```
+usage: datafetcher [-h]
+             (--product-locator PRODUCT_LOCATOR | --location-file LOCATION_FILE)
+             [--dry-run] [--output-dir OUTPUT_DIR] [--verbose]
+             [--profile PROFILE]
+
+Retrieve a product (a science product or an ancillary product) from the NRAO archive,
+either by specifying the product's locator or by providing the path to a product
+locator report.
+
+Optional Arguments:
+  --dry-run             dry run, do not fetch product
+  --output-dir OUTPUT_DIR
+                        output directory, default current directory
+  --verbose             make a lot of noise
+  --profile PROFILE     CAPO profile to use
+
+Return Codes:
+	1: no CAPO profile provided
+	2: missing required setting
+	3: request to locator service timed out
+	4: too many redirects on locator service
+	5: catastrophic error on request service
+	6: product locator not found
+	7: not able to open specified location file
+	8: error fetching file from NGAS server
+	9: retrieved file not expected size
+```
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..84415da77099fcd47dcf8120dd31be7b8a1f0b1c
--- /dev/null
+++ b/apps/cli/executables/null/setup.py
@@ -0,0 +1,44 @@
+#!/usr/bin/python
+# -*- coding: utf-8 -*-
+
+from pathlib import Path
+
+from setuptools import setup
+
+VERSION = open('_version.py').readlines()[-1].split()[-1].strip("\"'")
+README = Path('README.md').read_text()
+
+requires = [
+    'pika>=1.1,<2',
+    'pycapo>=0.3.0,<1.0',
+    'beautifulsoup4>=4.9.1,<5.0',
+    'lxml>=4.3.2,<5.0',
+    'psycopg2>=2.8.5,<3.0',
+    'pyopenssl>=19.1.0,<20.0',
+    'requests>=2.23,<3.0'
+]
+
+tests_require = [
+    'pytest>=5.4,<6.0'
+]
+setup(
+    name=Path().absolute().name,
+    version=VERSION,
+    description='NRAO Archive Data Fetcher Script',
+    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=['datafetcher'],
+    package_dir={'':'src'},
+    classifiers=[
+        'Programming Language :: Python :: 3.8'
+    ],
+    entry_points={
+        'console_scripts': ['datafetcher = datafetcher.commands: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..282c1cc6bbe792ca32ce0757c6c699a41be2c97e
--- /dev/null
+++ b/apps/cli/executables/null/src/null/null.py
@@ -0,0 +1,84 @@
+import os
+import sys
+import time
+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.DEBUG)
+
+class Null:
+    def __init__(self, args, verbose):
+        self.args = args
+        self.verbose = verbose
+        self.args_to_funcs = {
+            'greeting': self.print_greeting,
+            'exit': self.exit_with_failure,
+            'nap': self.take_nap,
+            'dump': self.dump_core
+        }
+
+    def print_greeting(self):
+        logger.debug("Hello, world!")
+        if self.verbose:
+            logger.debug("And goodbye, world...")
+
+    def exit_with_failure(self):
+        if self.verbose:
+            logger.error("Error purposefully induced.")
+        sys.exit('Exiting with status code -1')
+
+    def take_nap(self):
+        print(self.verbose)
+        if self.verbose:
+            print("wtf??")
+            logger.debug("Going to sleep...")
+        time.sleep(5)
+        if self.verbose:
+            logger.debug("Waking up.")
+
+    def dump_core(self):
+        if self.verbose:
+            logger.debug("Aborting and dumping core...", stack_info=True)
+        os.abort()
+
+    def execute(self):
+        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():
+    parser = argparse.ArgumentParser(description=_DESCRIPTION.format(version),
+                                     formatter_class=argparse.RawTextHelpFormatter)
+    options = parser.add_argument_group('options', 'settings for altered 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('-g', '--greeting', action='store_true',
+                           required=False, dest='greeting', default=False,
+                           help='print out a friendly greeting')
+    functions.add_argument('-e', '--exit', action='store_true',
+                           required=False, dest='exit', default=False,
+                           help='print error message and exit with status code -1')
+    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()
+    null = Null(args, args.verbose)
+    null.execute()
+
+if __name__ == '__main__':
+    main()
\ No newline at end of file