diff --git a/src/pyjen/job.py b/src/pyjen/job.py
index d7c03b0..8272f17 100644
--- a/src/pyjen/job.py
+++ b/src/pyjen/job.py
@@ -480,6 +480,14 @@ class Job(JenkinsAPI):
         jxml = JobXML(self.config_xml)
         return jxml.publishers
 
+    def add_publisher(self, publisher):
+        """Adds a new job publisher to this job
+
+        :param publisher: job publisher to add"""
+        jxml = JobXML(self.config_xml)
+        jxml.add_publisher(publisher.node)
+        self.config_xml = jxml.xml
+
     @property
     def properties(self):  # pragma: no cover
         """all plugins configured as extra configuration properties"""
diff --git a/src/pyjen/plugins/buildtriggerpublisher.py b/src/pyjen/plugins/buildtriggerpublisher.py
new file mode 100644
index 0000000..56850eb
--- /dev/null
+++ b/src/pyjen/plugins/buildtriggerpublisher.py
@@ -0,0 +1,77 @@
+"""Interface to the Jenkins 'build trigger' publishing plugin"""
+import xml.etree.ElementTree as ElementTree
+
+
+class BuildTriggerPublisher:
+    """Interface to the Jenkins 'build trigger' publishing plugin
+
+    This plugin is a default, built-in plugin which is part of the Jenkins core
+    """
+
+    def __init__(self, node):
+        """
+        :param node: XML node defining the settings for a this plugin
+        :type node: :class:`ElementTree.Element`
+        """
+        self._root = node
+        assert self._root.tag == self.get_jenkins_plugin_name()
+
+    @staticmethod
+    def get_jenkins_plugin_name():
+        """Gets the name of the Jenkins plugin associated with this PyJen plugin
+
+        This static method is used by the PyJen plugin API to associate this
+        class with a specific Jenkins plugin, as it is encoded in the config.xml
+
+        :rtype: :class:`str`
+        """
+        return "hudson.tasks.BuildTrigger"
+
+    @property
+    def job_names(self):
+        """Gets the names of 0 or more downstream jobs managed by this config
+
+        :rtype: :class:`list` of :class:`str`
+        """
+
+        children_node = self._root.find('childProjects')
+        return [i.strip() for i in children_node.text.split(",")]
+
+    @property
+    def node(self):
+        """Gets the XML node associated with this publisher
+
+        :rtype: :class:`ElementTree.Element`
+        """
+        return self._root
+
+    @staticmethod
+    def create(project_name):
+        """Factory method for creating a new build trigger
+
+        The default trigger will run when the parent job is successful
+
+        :param str project_name: name of the downstream job to be triggered
+        :rtype: :class:`pyjen.plugins.buildtriggerpublisher.BuildTriggerPublisher`
+        """
+        default_xml = """<hudson.tasks.BuildTrigger>
+<threshold>
+<name>SUCCESS</name>
+<ordinal>0</ordinal>
+<color>BLUE</color>
+<completeBuild>true</completeBuild>
+</threshold>
+</hudson.tasks.BuildTrigger>"""
+        root_node = ElementTree.fromstring(default_xml)
+
+        child = ElementTree.SubElement(root_node, "childProjects")
+        child.text = project_name
+
+        return BuildTriggerPublisher(root_node)
+
+
+PluginClass = BuildTriggerPublisher
+
+
+if __name__ == "__main__":  # pragma: no cover
+    pass
diff --git a/src/pyjen/utils/jobxml.py b/src/pyjen/utils/jobxml.py
index 3ffeae5..af20cac 100644
--- a/src/pyjen/utils/jobxml.py
+++ b/src/pyjen/utils/jobxml.py
@@ -144,6 +144,13 @@ class JobXML(ConfigXML):
                 self._log.warning("Unsupported job 'publisher' plugin")
         return retval
 
+    def add_publisher(self, node):
+        """Adds a new publisher node to the publisher section of the job XML
+
+        :param node: Elementree XML node for the publisher to assign"""
+        pubs = self._root.find('publishers')
+        pubs.append(node)
+
     @property
     def builders(self):
         """Gets a list of 0 or more build operations associated with this job

Add support for adding downstream build trigger to job
To facilitate testing the job.py module we need the ability to add downstream jobs as a build trigger publisher. 
diff --git a/tests/test_job.py b/tests/test_job.py
index bd023f9..7073ce9 100644
--- a/tests/test_job.py
+++ b/tests/test_job.py
@@ -281,12 +281,16 @@ def test_build_health(jenkins_env):
     finally:
         jb.delete()
 
-# TODO: add support for publishers to API so we can test upstream/downstream jobs
-# def test_publishers(jenkins_env):
-#     jk = Jenkins(jenkins_env["url"], (jenkins_env["admin_user"], jenkins_env["admin_token"]))
-#     jb = jk.find_job("third")
-#     for cur_pub in jb.publishers:
-#         print(cur_pub)
+
+def test_no_publishers(jenkins_env):
+    jk = Jenkins(jenkins_env["url"], (jenkins_env["admin_user"], jenkins_env["admin_token"]))
+    jb = jk.create_job("test_no_publishers", "project")
+    pubs = jb.publishers
+
+    assert isinstance(pubs, list)
+    assert len(pubs) == 0
+
+
 
 
 # -=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=-=
diff --git a/tests/test_job_publishers/__init__.py b/tests/test_job_publishers/__init__.py
new file mode 100644
index 0000000..e69de29
diff --git a/tests/test_job_publishers/test_build_trigger_publisher.py b/tests/test_job_publishers/test_build_trigger_publisher.py
new file mode 100644
index 0000000..dc321ec
--- /dev/null
+++ b/tests/test_job_publishers/test_build_trigger_publisher.py
@@ -0,0 +1,62 @@
+import time
+import pytest
+from pyjen.jenkins import Jenkins
+from pyjen.plugins.buildtriggerpublisher import BuildTriggerPublisher
+
+
+def test_add_build_trigger_publisher(jenkins_env):
+    jk = Jenkins(jenkins_env["url"], (jenkins_env["admin_user"], jenkins_env["admin_token"]))
+    upstream_name = "test_add_build_trigger_publisher1"
+    jb = jk.create_job(upstream_name, "project")
+    jb2 = None
+    try:
+        downstream_name = "test_add_build_trigger_publisher2"
+        jb2 = jk.create_job(downstream_name, "project")
+        publisher = BuildTriggerPublisher.create(downstream_name)
+        jb.add_publisher(publisher)
+        # TODO: Find more robust way to detect when asynchronous calls are finished
+        #       Idea: look at Jenkins server logs to see if there is status info
+        #       we can use to figure this out
+        time.sleep(5)
+
+        # Get a fresh copy of our job to ensure we have an up to date
+        # copy of the config.xml for the job
+        tmp = jk.find_job(upstream_name)
+        pubs = tmp.publishers
+
+        assert isinstance(pubs, list)
+        assert len(pubs) == 1
+        assert isinstance(pubs[0], BuildTriggerPublisher)
+        names = pubs[0].job_names
+        assert isinstance(names, list)
+        assert len(names) == 1
+        assert names[0] == downstream_name
+    finally:
+        jb.delete()
+        if jb2:
+            jb2.delete()
+
+
+def test_downstream_jobs(jenkins_env):
+    jk = Jenkins(jenkins_env["url"], (jenkins_env["admin_user"], jenkins_env["admin_token"]))
+    jb = jk.create_job("test_downstream_jobs1", "project")
+    jb2 = None
+    try:
+        expected_name = "test_downstream_jobs2"
+        jb2 = jk.create_job(expected_name, "project")
+        publisher = BuildTriggerPublisher.create(expected_name)
+        jb.add_publisher(publisher)
+        time.sleep(5)
+        res = jb.downstream_jobs
+
+        assert isinstance(res, list)
+        assert len(res) == 1
+        assert res[0].name == expected_name
+    finally:
+        jb.delete()
+        if jb2:
+            jb2.delete()
+
+
+if __name__ == "__main__":
+    pytest.main([__file__, "-v", "-s"])

pip install -e .[dev]
parse_log_pytest
pytest
pytest pytest-cov
3.6
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
alabaster==0.7.13
astroid==2.11.7
attrs @ file:///opt/conda/conda-bld/attrs_1642510447205/work
Babel==2.11.0
bleach==4.1.0
certifi==2021.5.30
cffi==1.15.1
charset-normalizer==2.0.12
colorama==0.4.5
coverage==6.2
cryptography==40.0.2
dill==0.3.4
distlib==0.3.9
docker==5.0.3
docutils==0.18.1
filelock==3.4.1
idna==3.10
imagesize==1.4.1
importlib-metadata==4.8.3
importlib-resources==5.4.0
iniconfig @ file:///home/linux1/recipes/ci/iniconfig_1610983019677/work
isort==5.10.1
jeepney==0.7.1
Jinja2==3.0.3
keyring==23.4.1
lazy-object-proxy==1.7.1
mando==0.7.1
MarkupSafe==2.0.1
mccabe==0.7.0
mock==5.2.0
more-itertools @ file:///tmp/build/80754af9/more-itertools_1637733554872/work
packaging @ file:///tmp/build/80754af9/packaging_1637314298585/work
pkginfo==1.10.0
platformdirs==2.4.0
pluggy @ file:///tmp/build/80754af9/pluggy_1615976315926/work
py @ file:///opt/conda/conda-bld/py_1644396412707/work
pycparser==2.21
Pygments==2.14.0
-e git+https://github.com/TheFriendlyCoder/pyjen.git@c771c7d59e710a93004d26bf33fc0b679fcab66f#egg=pyjen
pylint==2.13.9
pyparsing @ file:///tmp/build/80754af9/pyparsing_1635766073266/work
pytest==6.2.4
pytest-cov==4.0.0
pytz==2025.2
radon==6.0.1
readme-renderer==34.0
requests==2.27.1
requests-toolbelt==1.0.0
rfc3986==1.5.0
SecretStorage==3.3.3
six==1.17.0
snowballstemmer==2.2.0
Sphinx==5.3.0
sphinxcontrib-applehelp==1.0.2
sphinxcontrib-devhelp==1.0.2
sphinxcontrib-htmlhelp==2.0.0
sphinxcontrib-jsmath==1.0.1
sphinxcontrib-qthelp==1.0.3
sphinxcontrib-serializinghtml==1.1.5
toml @ file:///tmp/build/80754af9/toml_1616166611790/work
tomli==1.2.3
tox==3.28.0
tqdm==4.64.1
twine==3.8.0
typed-ast==1.5.5
typing_extensions @ file:///opt/conda/conda-bld/typing_extensions_1647553014482/work
urllib3==1.26.20
virtualenv==20.17.1
webencodings==0.5.1
websocket-client==1.3.1
wrapt==1.16.0
zipp @ file:///tmp/build/80754af9/zipp_1633618647012/work

name: pyjen
channels:
  - defaults
  - https://repo.anaconda.com/pkgs/main
  - https://repo.anaconda.com/pkgs/r
  - conda-forge
dependencies:
  - _libgcc_mutex=0.1=main
  - _openmp_mutex=5.1=1_gnu
  - attrs=21.4.0=pyhd3eb1b0_0
  - ca-certificates=2025.2.25=h06a4308_0
  - certifi=2021.5.30=py36h06a4308_0
  - importlib_metadata=4.8.1=hd3eb1b0_0
  - iniconfig=1.1.1=pyhd3eb1b0_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.3=he6710b0_2
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - more-itertools=8.12.0=pyhd3eb1b0_0
  - ncurses=6.4=h6a678d5_0
  - openssl=1.1.1w=h7f8727e_0
  - packaging=21.3=pyhd3eb1b0_0
  - pip=21.2.2=py36h06a4308_0
  - pluggy=0.13.1=py36h06a4308_0
  - py=1.11.0=pyhd3eb1b0_0
  - pyparsing=3.0.4=pyhd3eb1b0_0
  - pytest=6.2.4=py36h06a4308_2
  - python=3.6.13=h12debd9_1
  - readline=8.2=h5eee18b_0
  - setuptools=58.0.4=py36h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - toml=0.10.2=pyhd3eb1b0_0
  - typing_extensions=4.1.1=pyh06a4308_0
  - wheel=0.37.1=pyhd3eb1b0_0
  - xz=5.6.4=h5eee18b_1
  - zipp=3.6.0=pyhd3eb1b0_0
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - alabaster==0.7.13
      - astroid==2.11.7
      - babel==2.11.0
      - bleach==4.1.0
      - cffi==1.15.1
      - charset-normalizer==2.0.12
      - colorama==0.4.5
      - coverage==6.2
      - cryptography==40.0.2
      - dill==0.3.4
      - distlib==0.3.9
      - docker==5.0.3
      - docutils==0.18.1
      - filelock==3.4.1
      - idna==3.10
      - imagesize==1.4.1
      - importlib-metadata==4.8.3
      - importlib-resources==5.4.0
      - isort==5.10.1
      - jeepney==0.7.1
      - jinja2==3.0.3
      - keyring==23.4.1
      - lazy-object-proxy==1.7.1
      - mando==0.7.1
      - markupsafe==2.0.1
      - mccabe==0.7.0
      - mock==5.2.0
      - pkginfo==1.10.0
      - platformdirs==2.4.0
      - pycparser==2.21
      - pygments==2.14.0
      - pylint==2.13.9
      - pytest-cov==4.0.0
      - pytz==2025.2
      - radon==6.0.1
      - readme-renderer==34.0
      - requests==2.27.1
      - requests-toolbelt==1.0.0
      - rfc3986==1.5.0
      - secretstorage==3.3.3
      - six==1.17.0
      - snowballstemmer==2.2.0
      - sphinx==5.3.0
      - sphinxcontrib-applehelp==1.0.2
      - sphinxcontrib-devhelp==1.0.2
      - sphinxcontrib-htmlhelp==2.0.0
      - sphinxcontrib-jsmath==1.0.1
      - sphinxcontrib-qthelp==1.0.3
      - sphinxcontrib-serializinghtml==1.1.5
      - tomli==1.2.3
      - tox==3.28.0
      - tqdm==4.64.1
      - twine==3.8.0
      - typed-ast==1.5.5
      - urllib3==1.26.20
      - virtualenv==20.17.1
      - webencodings==0.5.1
      - websocket-client==1.3.1
      - wrapt==1.16.0
prefix: /opt/conda/envs/pyjen

tests/test_job.py::test_set_config_xml tests/test_job.py::test_one_downstream_job tests/test_job.py::test_multiple_downstream_jobs_recursive tests/test_job.py::test_one_upstream_job tests/test_job.py::test_multiple_upstream_jobs_recursive tests/test_job.py::test_get_one_recent_build tests/test_job.py::test_get_last_good_build tests/test_job.py::test_get_last_build tests/test_job.py::test_get_last_failed_build tests/test_job.py::test_get_last_stable_build tests/test_job.py::test_get_last_unsuccessful_build tests/test_job.py::test_get_build_by_number tests/test_job.py::test_get_builds_in_time_range_no_builds tests/test_job.py::test_get_builds_in_time_range_no_builds_in_range tests/test_job.py::test_get_builds_in_time_range_one_match tests/test_job.py::test_get_builds_in_time_range_inverted_parameters
src/pyjen/plugins/buildtriggerpublisher.py src/pyjen/utils/jobxml.py src/pyjen/job.py
src/pyjen/plugins/buildtriggerpublisher.py src/pyjen/utils/jobxml.py src/pyjen/job.py