diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 861cdd6..d32f204 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -5,6 +5,7 @@ repos:
     -   id: check-yaml
     -   id: end-of-file-fixer
     -   id: trailing-whitespace
+        exclude: tests/test_alerter.py
     -   id: flake8
 -   repo: https://github.com/psf/black
     rev: 19.10b0
diff --git a/docs/_includes/monitors.html b/docs/_includes/monitors.html
index dab1044..e8b9d83 100644
--- a/docs/_includes/monitors.html
+++ b/docs/_includes/monitors.html
@@ -21,6 +21,7 @@ All monitor types share the following configuration options:
 |recovered_command| A command to execute once when this monitor succeeds the first time after being failed.|no| |
 |group|The group the monitor belongs to. Alerters will only fire for monitors which appear in their groups.|no|`default`|
 |notify|If the monitor should alert at all|no|1|
+|failure_doc|Information to include in alerts on failure (e.g. a URL to a runbook)|no| |
 {% for monitor in m %}
 <a name="{{monitor.name}}"></a>
 
diff --git a/simplemonitor/Alerters/alerter.py b/simplemonitor/Alerters/alerter.py
index ecaf962..78d5d70 100644
--- a/simplemonitor/Alerters/alerter.py
+++ b/simplemonitor/Alerters/alerter.py
@@ -345,7 +345,13 @@ class Alerter:
                 Description: {desc}
                 """
                 if monitor.recover_info != "":
-                    message = message + "Recovery info: {}".format(monitor.recover_info)
+                    message = message + "Recovery info: {}\n".format(
+                        monitor.recover_info
+                    )
+                if monitor.failure_doc:
+                    message = message + "Documentation: {}\n".format(
+                        monitor.failure_doc
+                    )
             elif alert_type == AlertType.SUCCESS:
                 message = """
                 Monitor {monitor.name}{host} {alert_verb}!
diff --git a/simplemonitor/Monitors/monitor.py b/simplemonitor/Monitors/monitor.py
index 724bad3..5dbae2f 100644
--- a/simplemonitor/Monitors/monitor.py
+++ b/simplemonitor/Monitors/monitor.py
@@ -86,6 +86,9 @@ class Monitor:
         self.minimum_gap = self.get_config_option(
             "gap", required_type="int", minimum=0, default=0
         )
+        self.failure_doc = cast(
+            Optional[str], self.get_config_option("failure_doc", default=None)
+        )
 
         self.running_on = short_hostname()
         self._state = MonitorState.UNKNOWN

Support documentation fields in monitors
Add one or more documentation fields to Monitors, so they can include information such as a link to a run/playbook which Alerters can include.
diff --git a/tests/test_alerter.py b/tests/test_alerter.py
index 41612e8..471415e 100644
--- a/tests/test_alerter.py
+++ b/tests/test_alerter.py
@@ -1,5 +1,6 @@
 # type: ignore
 import datetime
+import textwrap
 import unittest
 
 from freezegun import freeze_time
@@ -288,6 +289,153 @@ class TestAlerter(unittest.TestCase):
             self.assertEqual(a._ooh_failures, [])
 
 
+class TestMessageBuilding(unittest.TestCase):
+    def test_notification_format_failure(self):
+        m = monitor.MonitorFail("test", {})
+        with freeze_time("2020-03-10 09:00"):
+            m.run_test()
+            self.assertEqual(
+                alerter.Alerter.build_message(
+                    alerter.AlertLength.NOTIFICATION, alerter.AlertType.FAILURE, m
+                ),
+                "Monitor test failed",
+            )
+
+    def test_notification_format_success(self):
+        m = monitor.MonitorNull("winning", {})
+        with freeze_time("2020-03-10 09:00"):
+            for _ in range(0, 6):
+                m.run_test()
+            self.assertEqual(
+                alerter.Alerter.build_message(
+                    alerter.AlertLength.NOTIFICATION, alerter.AlertType.SUCCESS, m
+                ),
+                "Monitor winning succeeded",
+            )
+
+    def test_oneline_format_failure(self):
+        m = monitor.MonitorFail("test", {})
+        with freeze_time("2020-03-10 09:00"):
+            m.run_test()
+            self.assertEqual(
+                alerter.Alerter.build_message(
+                    alerter.AlertLength.ONELINE, alerter.AlertType.FAILURE, m
+                ),
+                "failure: test failed on {hostname} at 2020-03-10 09:00:00 (0+00:00:00): This monitor always fails.".format(
+                    hostname=util.short_hostname()
+                ),
+            )
+
+    def test_oneline_format_success(self):
+        m = monitor.MonitorNull("winning", {})
+        with freeze_time("2020-03-10 09:00"):
+            for _ in range(0, 6):
+                m.run_test()
+            m.last_result = "a " * 80
+            desired = (
+                "success: winning succeeded on {hostname} at  (0+00:00:00): ".format(
+                    hostname=util.short_hostname()
+                )
+                + "a " * 80
+            )
+            output = alerter.Alerter.build_message(
+                alerter.AlertLength.ONELINE, alerter.AlertType.SUCCESS, m
+            )
+            self.assertEqual(desired, output)
+
+    def test_sms_format_failure(self):
+        m = monitor.MonitorFail("test", {})
+        with freeze_time("2020-03-10 09:00"):
+            m.run_test()
+            self.assertEqual(
+                alerter.Alerter.build_message(
+                    alerter.AlertLength.SMS, alerter.AlertType.FAILURE, m
+                ),
+                "failure: test failed on {hostname} at 2020-03-10 09:00:00 (0+00:00:00): This monitor always fails.".format(
+                    hostname=util.short_hostname()
+                ),
+            )
+
+    def test_sms_format_success(self):
+        m = monitor.MonitorNull("winning", {})
+        with freeze_time("2020-03-10 09:00"):
+            for _ in range(0, 6):
+                m.run_test()
+            m.last_result = "a " * 80
+            self.assertEqual(
+                alerter.Alerter.build_message(
+                    alerter.AlertLength.SMS, alerter.AlertType.SUCCESS, m
+                ),
+                "success: winning succeeded on {hostname} at (0+00:00:00): a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a a...".format(
+                    hostname=util.short_hostname()
+                ),
+            )
+
+    def test_full_format_failure(self):
+        m = monitor.MonitorFail("test", {})
+        with freeze_time("2020-03-10 09:00"):
+            m.run_test()
+            self.assertEqual(
+                alerter.Alerter.build_message(
+                    alerter.AlertLength.FULL, alerter.AlertType.FAILURE, m
+                ),
+                textwrap.dedent(
+                    """
+                    Monitor test on {hostname} failed!
+                    Failed at: 2020-03-10 09:00:00 (down 0+00:00:00)
+                    Virtual failure count: 1
+                    Additional info: This monitor always fails.
+                    Description: A monitor which always fails.
+                    """.format(
+                        hostname=util.short_hostname()
+                    )
+                ),
+            )
+
+    def test_full_format_failure_docs(self):
+        m = monitor.MonitorFail("test", {"failure_doc": "whoops"})
+        with freeze_time("2020-03-10 09:00"):
+            m.run_test()
+            self.assertEqual(
+                alerter.Alerter.build_message(
+                    alerter.AlertLength.FULL, alerter.AlertType.FAILURE, m
+                ),
+                textwrap.dedent(
+                    """
+                    Monitor test on {hostname} failed!
+                    Failed at: 2020-03-10 09:00:00 (down 0+00:00:00)
+                    Virtual failure count: 1
+                    Additional info: This monitor always fails.
+                    Description: A monitor which always fails.
+                    Documentation: whoops
+                    """.format(
+                        hostname=util.short_hostname()
+                    )
+                ),
+            )
+
+    def test_full_format_success(self):
+        m = monitor.MonitorNull("winning", {})
+        with freeze_time("2020-03-10 09:00"):
+            for _ in range(0, 6):
+                m.run_test()
+            self.assertEqual(
+                alerter.Alerter.build_message(
+                    alerter.AlertLength.FULL, alerter.AlertType.SUCCESS, m
+                ),
+                textwrap.dedent(
+                    """
+                    Monitor winning on {hostname} succeeded!
+                    Recovered at: 
+                    Additional info: 
+                    Description: (Monitor did not write an auto-biography.)
+                    """.format(  # noqa: W291
+                        hostname=util.short_hostname()
+                    )
+                ),
+            )
+
+
 class TestSNSAlerter(unittest.TestCase):
     def test_config(self):
         with self.assertRaises(util.AlerterConfigurationError):

pip install -e .[dev]
parse_log_pytest
requirements.txt
pytest freezegun
apt-get update apt-get install -y gcc
3.9
requirements.txt
pytest --no-header -rA --tb=line --color=no -p no:cacheprovider -W ignore::DeprecationWarning
boto3==1.12.5
botocore==1.15.5
certifi==2019.11.28
cffi==1.14.0
chardet==3.0.4
colorlog==4.1.0
cryptography==2.8
docutils==0.15.2
exceptiongroup==1.2.2
freezegun==1.5.1
idna==2.8
iniconfig==2.1.0
jmespath==0.9.4
oauthlib==3.1.0
packaging==24.2
paho-mqtt==1.5.0
pluggy==1.5.0
psutil==5.7.0
pycparser==2.19
pyOpenSSL==19.1.0
pytest==8.3.5
python-dateutil==2.8.1
pytz==2019.3
requests==2.22.0
requests-oauthlib==1.3.0
ring_doorbell==0.6.0
s3transfer==0.3.3
-e git+https://github.com/jamesoff/simplemonitor.git@f38b86b3061a605aa34a881b85bb40c3c9369bbd#egg=simplemonitor
six==1.14.0
tomli==2.2.1
urllib3==1.25.8

name: simplemonitor
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
  - ca-certificates=2025.2.25=h06a4308_0
  - ld_impl_linux-64=2.40=h12ee557_0
  - libffi=3.4.4=h6a678d5_1
  - libgcc-ng=11.2.0=h1234567_1
  - libgomp=11.2.0=h1234567_1
  - libstdcxx-ng=11.2.0=h1234567_1
  - ncurses=6.4=h6a678d5_0
  - openssl=3.0.16=h5eee18b_0
  - pip=25.0=py39h06a4308_0
  - python=3.9.21=he870216_1
  - readline=8.2=h5eee18b_0
  - setuptools=75.8.0=py39h06a4308_0
  - sqlite=3.45.3=h5eee18b_0
  - tk=8.6.14=h39e8969_0
  - tzdata=2025a=h04d1e81_0
  - wheel=0.45.1=py39h06a4308_0
  - xz=5.6.4=h5eee18b_1
  - zlib=1.2.13=h5eee18b_1
  - pip:
      - boto3==1.12.5
      - botocore==1.15.5
      - certifi==2019.11.28
      - cffi==1.14.0
      - chardet==3.0.4
      - colorlog==4.1.0
      - cryptography==2.8
      - docutils==0.15.2
      - exceptiongroup==1.2.2
      - freezegun==1.5.1
      - idna==2.8
      - iniconfig==2.1.0
      - jmespath==0.9.4
      - oauthlib==3.1.0
      - packaging==24.2
      - paho-mqtt==1.5.0
      - pluggy==1.5.0
      - psutil==5.7.0
      - pycparser==2.19
      - pyopenssl==19.1.0
      - pytest==8.3.5
      - python-dateutil==2.8.1
      - pytz==2019.3
      - requests==2.22.0
      - requests-oauthlib==1.3.0
      - ring-doorbell==0.6.0
      - s3transfer==0.3.3
      - six==1.14.0
      - tomli==2.2.1
      - urllib3==1.25.8
prefix: /opt/conda/envs/simplemonitor

tests/test_alerter.py::TestMessageBuilding::test_full_format_failure_docs
tests/test_alerter.py::TestMessageBuilding::test_sms_format_success
tests/test_alerter.py::TestAlerter::test_allowed_always tests/test_alerter.py::TestAlerter::test_allowed_default tests/test_alerter.py::TestAlerter::test_allowed_not tests/test_alerter.py::TestAlerter::test_allowed_only tests/test_alerter.py::TestAlerter::test_allowed_today tests/test_alerter.py::TestAlerter::test_days tests/test_alerter.py::TestAlerter::test_delay tests/test_alerter.py::TestAlerter::test_dependencies tests/test_alerter.py::TestAlerter::test_dryrun tests/test_alerter.py::TestAlerter::test_groups tests/test_alerter.py::TestAlerter::test_limit tests/test_alerter.py::TestAlerter::test_not_allowed_today tests/test_alerter.py::TestAlerter::test_oohrecovery tests/test_alerter.py::TestAlerter::test_repeat tests/test_alerter.py::TestAlerter::test_should_alert_basic_failure tests/test_alerter.py::TestAlerter::test_should_alert_basic_none tests/test_alerter.py::TestAlerter::test_should_alert_basic_success tests/test_alerter.py::TestAlerter::test_should_alert_catchup tests/test_alerter.py::TestAlerter::test_should_alert_limit tests/test_alerter.py::TestAlerter::test_should_alert_limit_ooh tests/test_alerter.py::TestAlerter::test_should_alert_no_catchup tests/test_alerter.py::TestAlerter::test_should_alert_only_failure tests/test_alerter.py::TestAlerter::test_should_alert_ooh tests/test_alerter.py::TestAlerter::test_should_alert_unavailable tests/test_alerter.py::TestAlerter::test_should_not_alert_basic_success tests/test_alerter.py::TestAlerter::test_should_not_alert_ooh tests/test_alerter.py::TestAlerter::test_times_always tests/test_alerter.py::TestAlerter::test_times_broken tests/test_alerter.py::TestAlerter::test_times_not tests/test_alerter.py::TestAlerter::test_times_only tests/test_alerter.py::TestMessageBuilding::test_full_format_failure tests/test_alerter.py::TestMessageBuilding::test_full_format_success tests/test_alerter.py::TestMessageBuilding::test_notification_format_failure tests/test_alerter.py::TestMessageBuilding::test_notification_format_success tests/test_alerter.py::TestMessageBuilding::test_oneline_format_failure tests/test_alerter.py::TestMessageBuilding::test_oneline_format_success tests/test_alerter.py::TestMessageBuilding::test_sms_format_failure tests/test_alerter.py::TestSNSAlerter::test_config
simplemonitor/Alerters/alerter.py docs/_includes/monitors.html .pre-commit-config.yaml simplemonitor/Monitors/monitor.py
simplemonitor/Alerters/alerter.py docs/_includes/monitors.html .pre-commit-config.yaml simplemonitor/Monitors/monitor.py