diff --git a/CHANGES.md b/CHANGES.md
index 7cdbd2e6c77..0ded1be62bc 100644
--- a/CHANGES.md
+++ b/CHANGES.md
@@ -47,6 +47,19 @@ creating a new release entry be sure to copy & paste the span tag with the
`actions:bind` attribute, which is used by a regex to find the text to be
updated. Only the first match gets replaced, so it's fine to leave the old
ones in. -->
+-------------------------------------------------------------------------------
+## __cylc-8.0b3 (???)__
+
+Fourth beta release of Cylc 8.
+
+(See note on cylc-8 backward-incompatible changes, above)
+
+### Enhancements
+
+[#4335](https://github.com/cylc/cylc-flow/pull/4335) - have validation catch
+erroneous use of both `expr => bar` and `expr => !bar` in the same graph.
+
+
-------------------------------------------------------------------------------
## __cylc-8.0b2 (Released 2021-07-28)__
diff --git a/cylc/flow/graph_parser.py b/cylc/flow/graph_parser.py
index 91f688c5a5b..9474e8629eb 100644
--- a/cylc/flow/graph_parser.py
+++ b/cylc/flow/graph_parser.py
@@ -16,6 +16,7 @@
"""Module for parsing cylc graph strings."""
import re
+import contextlib
from cylc.flow.exceptions import GraphParseError
from cylc.flow.param_expand import GraphExpander
@@ -506,6 +507,18 @@ def _add_trigger(self, orig_expr, rights, expr, info):
else:
members = [right]
for member in members:
+ with contextlib.suppress(KeyError):
+ osuicide = self.triggers[member][expr][1]
+ # This trigger already exists, so we must have both
+ # "expr => member" and "expr => !member" in the graph,
+ # or simply a duplicate trigger not recognized earlier
+ # because of parameter offsets.
+ if suicide or osuicide:
+ oexp = re.sub(r'(&|\|)', r' \1 ', orig_expr)
+ oexp = re.sub(r':succeed', '', oexp)
+ raise GraphParseError(
+ f"{oexp} can't trigger both {member} and !{member}"
+ )
self.triggers.setdefault(member, {})
self.original.setdefault(member, {})
self.triggers[member][expr] = (trigs, suicide)
diff --git a/tests/unit/test_graph_parser.py b/tests/unit/test_graph_parser.py
index 618852fcc97..153f3c1c210 100644
--- a/tests/unit/test_graph_parser.py
+++ b/tests/unit/test_graph_parser.py
@@ -44,6 +44,16 @@ def test_parse_graph_fails_with_spaces_in_task_name(self):
with self.assertRaises(GraphParseError):
self.parser.parse_graph("a b => c")
+ def test_parse_graph_fails_with_suicide_and_not_suicide(self):
+ """Test graph parser fails with both "expr => !foo"
+ and "expr => !foo" in the same graph."""
+ with self.assertRaises(GraphParseError):
+ self.parser.parse_graph(
+ """(a | b & c) => d
+ foo => bar
+ (a | b & c) => !d
+ """)
+
def test_parse_graph_fails_with_invalid_and_operator(self):
"""Test that the graph parse will fail when the and operator is not
correctly used."""