diff --git a/.gitignore b/.gitignore
--- a/.gitignore
+++ b/.gitignore
@@ -23,3 +23,6 @@
 
 # local configuration files
 /conf/trigger.cfg
+/conf/trigger_rules.yml
+/docker_data/trigger.cfg
+/docker_data/trigger_rules.yml
diff --git a/Dockerfile b/Dockerfile
--- a/Dockerfile
+++ b/Dockerfile
@@ -1,20 +1,15 @@
 FROM fedora:24
 
-# This is a workaround, since for some weird reason, if all but taskotron-trigger gets installed from the 
-# repos, then fedmsg-hub only sees one consumer
-# But when installed from repos, it works fine, even though there is no relevant difference on the disk
-ADD ./docker_data/tflink-taskotron-fedora.repo /etc/yum.repos.d/
-RUN dnf install -y taskotron-trigger && dnf clean all
-#RUN dnf install -y fedmsg fedmsg-hub git koji python-requests python-twisted PyYAML && dnf clean all &&\
-#    mkdir -p /etc/taskotron /var/log/taskotron-trigger /var/lib/taskotron-trigger
+RUN dnf install -y fedmsg fedmsg-hub git koji python2-requests python2-twisted PyYAML && dnf clean all &&\
+    mkdir -p /etc/taskotron /var/log/taskotron-trigger /var/lib/taskotron-trigger
 
 COPY ./conf/trigger.cfg.example /etc/taskotron/trigger.cfg
+COPY ./conf/trigger_rules.yml.example /etc/taskotron/trigger_rules.yml
 COPY ./docker_data/critpath_whitelist.example /var/lib/taskotron-trigger/critpath_whitelist
 COPY ./fedmsg.d/taskotron-trigger.py /etc/fedmsg.d/
 COPY . /usr/src/taskotron-trigger
 
 WORKDIR /usr/src/taskotron-trigger
-RUN python setup.py build
-RUN python setup.py install -O1 --skip-build --root /
+RUN python setup.py build && python setup.py install -O1 --skip-build --root /
 
 CMD ["/usr/bin/fedmsg-hub"]
diff --git a/conf/trigger.cfg.example b/conf/trigger.cfg.example
--- a/conf/trigger.cfg.example
+++ b/conf/trigger.cfg.example
@@ -2,25 +2,16 @@
 url = http://localhost:8080/buildbot/change_hook
 
 [trigger]
-koji_build_completed_tasks = rpmlint
-docker_build_tasks = dockerautotest
-koji_tag_changed_tasks = upgradepath,depcheck
-compose_completed_tasks =
 valid_arches = x86_64
 koji_url = http://koji.fedoraproject.org/kojihub
 datagrepper_url = https://apps.fedoraproject.org/datagrepper/raw
 execdb_server = http://localhost:5003
-type = StreamTrigger
+runner_type = StreamRunner
 job_logging = False
 joblog_file = /var/log/taskotron-trigger/jobs.csv
 fuse_delay = 1000 ; seconds
+git_cache_dir = /var/lib/taskotron-trigger/cache
+rules_template = /etc/taskotron/trigger_rules.yml
 
-[distgit]
-enabled = True
-cache_dir = /var/lib/taskotron-trigger/cache
-repo_url = http://pkgs.fedoraproject.org/git
-namespace = rpms-checks
-
-[abicheck]
+[koji_build_completed]
 critpath_filepath = /var/lib/taskotron-trigger/critpath_whitelist
-critpath_exclude =
diff --git a/conf/trigger_rules.yml.example b/conf/trigger_rules.yml.example
new file mode 100644
--- /dev/null
+++ b/conf/trigger_rules.yml.example
@@ -0,0 +1,23 @@
+---
+- when: {message_type: KojiBuildCompleted}
+  do:
+    - {tasks: [rpmlint, rpmgrill]}
+    - {discover: {repo: 'http://pkgs.fedoraproject.org/git/rpms-checks/${name}.git', branch: "${distgit_branch}"}}
+- when: {message_type: KojiBuildCompleted, name: docker}
+  do:
+    - {tasks: [dockerautotest]}
+- when:
+    message_type: KojiBuildCompleted
+    name:
+      $in: ${critpath_pkgs}
+      $nin: []
+  do:
+    - {tasks: [abicheck]}
+- when: {message_type: KojiTagChanged}
+  do:
+    - {tasks: [depcheck]}
+- when:
+    message_type: KojiTagChanged
+    tag: {$regex: '/.*(?<!testing-pending)$$/'}
+  do:
+    - {tasks: [upgradepath]}
diff --git a/docker_data/tflink-taskotron-fedora.repo b/docker_data/tflink-taskotron-fedora.repo
deleted file mode 100644
--- a/docker_data/tflink-taskotron-fedora.repo
+++ /dev/null
@@ -1,8 +0,0 @@
-[tflink-taskotron]
-name=Copr repo for taskotron owned by tflink
-baseurl=https://copr-be.cloud.fedoraproject.org/results/tflink/taskotron/fedora-$releasever-$basearch/
-skip_if_unavailable=True
-gpgcheck=1
-gpgkey=https://copr-be.cloud.fedoraproject.org/results/tflink/taskotron/pubkey.gpg
-enabled=1
-enabled_metadata=1
diff --git a/jobtriggers/__init__.py b/jobtriggers/__init__.py
--- a/jobtriggers/__init__.py
+++ b/jobtriggers/__init__.py
@@ -1,23 +0,0 @@
-import itertools
-import fedmsg.consumers
-from . import config, triggers, utils
-
-
-class JobTrigger(fedmsg.consumers.FedmsgConsumer):
-    def __init__(self, *args, **kw):
-        super(JobTrigger, self).__init__(*args, **kw)
-        self.trigger = triggers.get_trigger(config.trigger_type)
-
-    def trigger_tasks(self, item, item_type, tasks, arches, **kwargs):
-        for task, arch in itertools.product(tasks, arches):
-            self.log.info('triggering %s for %s on %s', task, item, arch)
-
-            if config.job_logging:
-                try:
-                    utils.log_job(config.joblog_file, item, item_type, task, arch)
-                except IOError, e:
-                    self.log.exception(e)
-
-            output = self.trigger.trigger_job(item, item_type, task, arch, **kwargs)
-            for line in output:
-                self.log.info(line)
diff --git a/jobtriggers/compose_complete_msg.py b/jobtriggers/compose_complete_msg.py
--- a/jobtriggers/compose_complete_msg.py
+++ b/jobtriggers/compose_complete_msg.py
@@ -1,8 +1,7 @@
 import fedmsg
 import fedmsg.encoding
 
-from . import config
-from . import JobTrigger
+from .jobtrigger import JobTrigger
 
 ITEM_TYPE = 'compose'
 
@@ -16,20 +15,27 @@
         super(ComposeCompletedJobTrigger, self).__init__(*args, **kw)
 
     def consume(self, message):
-        usable_message = fedmsg.encoding.loads(message.body)
+        msg = fedmsg.encoding.loads(message.body)
 
-        topic = usable_message['topic']
+        topic = msg['topic']
 
         if not 'branched.complete' in topic and not 'rawhide.complete' in topic:
             return
 
-        branch = usable_message['msg']['branch']
-        arch = usable_message['msg']['arch']
+        branch = msg['msg']['branch']
+        arch = msg['msg']['arch']
 
         # non empty arch means secondary arch
         if arch:
             return
 
         self.log.debug('should be scheduling job for completed compose: %s' % branch)
 
-        self.trigger_tasks(branch, ITEM_TYPE, config.compose_completed_tasks, ['noarch'])
+        data = {
+            "_msg": msg,
+            "message_type": "ComposeCompleted",
+            "item": branch,
+            "item_type": ITEM_TYPE,
+            "branch": branch,
+        }
+        self.do_trigger(data)
diff --git a/jobtriggers/config.py b/jobtriggers/config.py
--- a/jobtriggers/config.py
+++ b/jobtriggers/config.py
@@ -6,28 +6,19 @@
 url = http://localhost:8080/buildbot/change_hook
 
 [trigger]
-koji_build_completed_tasks = rpmlint
-docker_build_tasks = dockerautotest
-koji_tag_changed_tasks = upgradepath,depcheck
-compose_completed_tasks =
 valid_arches = x86_64
 koji_url = http://koji.fedoraproject.org/kojihub
 datagrepper_url = https://apps.fedoraproject.org/datagrepper/raw
 execdb_server = http://localhost:5003
-type = StreamTrigger
+runner_type = StreamRunner
 job_logging = False
 joblog_file = /var/log/taskotron-trigger/jobs.csv
 fuse_delay = 1000 ; seconds
+git_cache_dir = /var/lib/taskotron-trigger/cache
+rules_template = ./conf/trigger_rules.yml.example
 
-[distgit]
-enabled = True
-cache_dir = /var/lib/taskotron-trigger/cache
-repo_url = http://pkgs.fedoraproject.org/git
-namespace = rpms-checks
-
-[abicheck]
+[koji_build_completed]
 critpath_filepath = /var/lib/taskotron-trigger/critpath_whitelist
-critpath_exclude =
 """
 
 config = ConfigParser.ConfigParser()
@@ -37,23 +28,17 @@
 
 buildbot_url = config.get('buildbot', 'url')
 
-koji_build_completed_tasks = [e.strip() for e in config.get('trigger', 'koji_build_completed_tasks').split(',') if e]
-docker_build_tasks = [e.strip() for e in config.get('trigger', 'docker_build_tasks').split(',') if e]
-koji_tag_changed_tasks = [e.strip() for e in config.get('trigger', 'koji_tag_changed_tasks').split(',') if e]
-compose_completed_tasks = [e.strip() for e in config.get('trigger', 'compose_completed_tasks').split(',') if e]
 valid_arches = [e.strip() for e in config.get('trigger', 'valid_arches').split(',') if e]
 koji_url = config.get('trigger', 'koji_url')
 datagrepper_url = config.get('trigger', 'datagrepper_url')
 execdb_server = config.get('trigger', 'execdb_server')
-trigger_type = config.get('trigger', 'type')
+runner_type = config.get('trigger', 'runner_type')
 job_logging = True if config.get('trigger', 'job_logging') == 'True' else False
 joblog_file = config.get('trigger', 'joblog_file')
 fuse_delay = int(config.get('trigger', 'fuse_delay'))  # seconds
+git_cache_dir = config.get('trigger', 'git_cache_dir')
+rules_template_file = config.get('trigger', 'rules_template')
 
-distgit_enabled = True if config.get('distgit', 'enabled') == 'True' else False
-distgit_cache_dir = config.get('distgit', 'cache_dir')
-distgit_repo_url = config.get('distgit', 'repo_url')
-distgit_namespace = config.get('distgit', 'namespace')
+critpath_filepath = config.get('koji_build_completed', 'critpath_filepath')
 
-critpath_filepath = config.get('abicheck', 'critpath_filepath')
-critpath_exclude = [e.strip() for e in config.get('abicheck', 'critpath_exclude').split(',') if e]
+trigger_rules_template = open(rules_template_file, 'r').read()
diff --git a/jobtriggers/exceptions.py b/jobtriggers/exceptions.py
--- a/jobtriggers/exceptions.py
+++ b/jobtriggers/exceptions.py
@@ -1,3 +1,7 @@
+class RunnerException(Exception):
+    pass
+
+
 class TriggerError(Exception):
     pass
 
diff --git a/jobtriggers/jobrunner.py b/jobtriggers/jobrunner.py
--- a/jobtriggers/jobrunner.py
+++ b/jobtriggers/jobrunner.py
@@ -9,10 +9,10 @@
 from . import koji_build_msg
 from . import koji_tag_msg
 from . import exceptions as exc
-from .triggers import get_trigger
+from .runners import get_runner
 
 
-trigger = get_trigger(config.trigger_type)
+runner = get_runner(config.runner_type)
 
 # set up logging
 logger = logging.getLogger('Jobrunner')
@@ -26,7 +26,7 @@
 def trigger_task(item, item_type, task, arch, **kwargs):
     logger.debug('triggering %s for %s on %s' % (task, item, arch))
 
-    output = trigger.trigger_job(item, item_type, task, arch, **kwargs)
+    output = runner.trigger_job(item, item_type, task, arch, **kwargs)
     for line in output:
         logger.info(line)
 
diff --git a/jobtriggers/jobtrigger.py b/jobtriggers/jobtrigger.py
new file mode 100644
--- /dev/null
+++ b/jobtriggers/jobtrigger.py
@@ -0,0 +1,119 @@
+import copy
+import yaml
+import itertools
+import fedmsg.consumers
+from . import config, runners, utils
+from . import exceptions as exc
+from .mongoquery import Query, QueryError
+from .mongoquery_string_template import MongoTemplate
+
+
+class DefaultDict(dict):
+
+    """Since the rules will contain unreplacable variables (for example KojiBuildCompleted
+    provides ${critpath_pkgs} and KojiTagChanged does not), we need a default value.
+    None/null would be probably safer, but to make debugging easier, let's replace the missing
+    keys with stringified placeholder instead.
+    """
+
+    def __missing__(self, key):
+        return str("MISSING_VAR_%s" % key)
+
+
+class JobTrigger(fedmsg.consumers.FedmsgConsumer):
+
+    def __init__(self, *args, **kw):
+        super(JobTrigger, self).__init__(*args, **kw)
+        self.runner = runners.get_runner(config.runner_type)
+
+    def trigger_tasks(self, item, item_type, tasks, arches, **kwargs):
+        for task, arch in itertools.product(tasks, arches):
+            self.log.info('triggering %s for %s on %s', task, item, arch)
+
+            if config.job_logging:
+                try:
+                    utils.log_job(config.joblog_file, item, item_type, task, arch)
+                except IOError, e:
+                    self.log.exception(e)
+
+            output = self.runner.trigger_job(item, item_type, task, arch, **kwargs)
+            for line in output:
+                self.log.info(line)
+
+    def _tasks_from_rule(self, rule, message_data):
+        """Takes a rule-dict as input, and produces list of dicts representing arguments required
+        by the trigger_tasks method (i.e. item, item_type, tasks, arches) plus any additional data
+        provided in the rule.
+
+        """
+
+        try:
+            task_template = {
+                'item': message_data['item'],
+                'item_type': message_data['item_type'],
+                'arches': config.valid_arches,
+                'tasks': []
+            }
+        except KeyError, e:
+            raise exc.TriggerMsgError("Missing data from message: %s" % e)
+
+        tasks = []
+        for item in rule['do']:
+            # TODO: make this more efficient
+            task = copy.deepcopy(task_template)
+
+            if 'tasks' in item:
+                task['tasks'] = item['tasks']
+            elif 'discover' in item:
+                discover = item['discover']
+                repo = discover['repo']
+                branch = discover.get('branch', 'master')
+                task['tasks'] = utils.discover_tasks(task['item_type'], repo, branch)
+                task['repo'] = repo
+                task['branch'] = branch
+            else:
+                self.log.error("Rule has invalid `do` section: %r", rule)
+
+            tasks.append(task)
+
+        return tasks
+
+    def _load_rules(self, template, data):
+        """Taken out for ease of testing. This method takes the rules template and
+        (message) data, replaces the variables in the template, and finally puts
+        the string (now containing data provided from the respective FedMsg consumer),
+        to the yaml parser.
+        Keys that would be needed in template, but are not present in data are replaced
+        by the default value provided by DefaultDict"""
+
+        rules_yaml = MongoTemplate(template).substitute(DefaultDict(data))
+
+        try:
+            rules = yaml.load(rules_yaml)
+        except yaml.YAMLError, e:
+            raise exc.TriggerError(e)
+        return rules
+
+    def do_trigger(self, message_data):
+        rules = self._load_rules(config.trigger_rules_template, message_data)
+
+        tasks = []
+        for rule in rules:
+            if not rule.get('enabled', True):
+                continue
+            if 'message_type' in rule['when']:
+                if rule['when']['message_type'] != message_data['message_type']:
+                    continue
+            try:
+                if Query(rule['when']).match(message_data):
+                    tasks.extend(self._tasks_from_rule(rule, message_data))
+            except QueryError, e:
+                self.log.debug(e)
+
+        for task in tasks:
+            try:
+                self.trigger_tasks(**task)
+            except exc.TriggerMsgError, e:
+                self.log.debug(e)
+            except exc.TriggerError, e:
+                self.log.error(e)
diff --git a/jobtriggers/koji_build_msg.py b/jobtriggers/koji_build_msg.py
--- a/jobtriggers/koji_build_msg.py
+++ b/jobtriggers/koji_build_msg.py
@@ -2,8 +2,9 @@
 import fedmsg
 import fedmsg.encoding
 
-from jobtriggers import config, utils, JobTrigger
-from jobtriggers import exceptions as exc
+from . import config, utils
+from .jobtrigger import JobTrigger
+from . import exceptions as exc
 
 ITEM_TYPE = 'koji_build'
 
@@ -33,61 +34,33 @@
     return name, version, release
 
 
-def trigger_tasks(msg, f):
-    name, version, release = get_nvr(msg)
-
-    branch = utils.get_branch(release)
-
-    # run dist-git package-specific
-    if config.distgit_enabled:
-        repo = utils.get_repo(name)
-        distgit_tasks = utils.get_distgit_tasks(name, ITEM_TYPE, repo, branch)
-        f('-'.join((name, version, release)),
-          ITEM_TYPE,
-          distgit_tasks,
-          config.valid_arches,
-          branch=branch,
-          repo=repo)
-
-    # run general qa tasks
-    f('-'.join((name, version, release)),
-      ITEM_TYPE,
-      config.koji_build_completed_tasks,
-      config.valid_arches)
-
-    # run docker-specific tasks here - e. g. dockerautotest
-    if name == "docker":
-        f('-'.join((name, version, release)),
-          ITEM_TYPE,
-          config.docker_build_tasks,
-          config.valid_arches)
-
-    # run abicheck task here
-    critpath = utils.parse_yaml_from_file(config.critpath_filepath)
-    critpath_pkgs = critpath['pkgs'].get(branch, critpath['pkgs']['master'])
-    abicheck_whitelist = set(critpath_pkgs) - set(config.critpath_exclude)
-
-    if name in abicheck_whitelist:
-        f('-'.join((name, version, release)),
-          ITEM_TYPE,
-          ['abicheck'],
-          config.valid_arches)
-
-
 class KojiBuildCompletedJobTrigger(JobTrigger):
     topic = "org.fedoraproject.prod.buildsys.build.state.change"
     jsonify = False
     config_key = 'taskotron.kojibuildcompletedjobtrigger.enabled'
 
-    def __init__(self, *args, **kw):
-        super(KojiBuildCompletedJobTrigger, self).__init__(*args, **kw)
-
     def consume(self, message):
         msg = fedmsg.encoding.loads(message.body)
 
         try:
-            trigger_tasks(msg, self.trigger_tasks)
+            name, version, release = get_nvr(msg)
         except exc.TriggerMsgError, e:
             self.log.debug(e)
-        except exc.TriggerError, e:
-            self.log.error(e)
+            return
+
+        item = '-'.join([name, version, release])
+        critpath = utils.parse_yaml_from_file(config.critpath_filepath)
+        distgit_branch = utils.get_distgit_branch(release)
+        critpath_pkgs = critpath['pkgs'].get(distgit_branch, critpath['pkgs']['master'])
+        data = {
+            "_msg": msg,
+            "message_type": "KojiBuildCompleted",
+            "item": item,
+            "item_type": ITEM_TYPE,
+            "name": name,
+            "version": version,
+            "release": release,
+            "critpath_pkgs": critpath_pkgs,
+            "distgit_branch": distgit_branch,
+        }
+        self.do_trigger(data)
diff --git a/jobtriggers/koji_tag_msg.py b/jobtriggers/koji_tag_msg.py
--- a/jobtriggers/koji_tag_msg.py
+++ b/jobtriggers/koji_tag_msg.py
@@ -1,13 +1,13 @@
 import re
-from copy import deepcopy
 
 import fedmsg
 import fedmsg.encoding
 
 from twisted.internet.task import LoopingCall
 
-from jobtriggers import config, JobTrigger
-from jobtriggers import exceptions as exc
+from . import config
+from .jobtrigger import JobTrigger
+from . import exceptions as exc
 
 ITEM_TYPE = 'koji_tag'
 
@@ -40,36 +40,41 @@
         super(KojiTagChangedJobTrigger, self).__init__(*args, **kw)
 
         self.queued_tags = set()
-
+        self.queued_data = dict()
         # check for bodhi_to_koji_tags tasks queue every config.fuse_delay seconds
         lc = LoopingCall(self.delayed_consume)
         lc.start(config.fuse_delay)
 
     def delayed_consume(self):
         if self.queued_tags:
             try:
-                for tag in self.queued_tags:
-                    # XXX ugly: do not schedule upgradepath for updates-testing-pending
-                    tasks = deepcopy(config.koji_tag_changed_tasks)
-                    if tag.endswith('testing-pending'):
-                        tasks.remove('upgradepath')
-                    self.trigger_tasks(tag, ITEM_TYPE, tasks, config.valid_arches)
+                for tag, data in self.queued_data.items():
+                    self.do_trigger(data)
             finally:
+                self.queued_data = dict()
                 self.queued_tags = set()
         else:
             self.log.debug("Woke up, but there were no messages.")
 
     def consume(self, message):
         msg = fedmsg.encoding.loads(message.body)
-
         try:
             item = get_item(msg)
+            if item in self.queued_tags:
+                self.log.info("koji_tag %s already in the queue, skipping", item)
+                return
+
+            self.log.info('Adding koji_tag %s to the queue', item)
 
-            if not len(self.queued_tags):
-                self.log.info('setting fuse (%d seconds) for koji_tag %s', config.fuse_delay, item)
-            elif item not in self.queued_tags:
-                self.log.info('adding koji_tag %s to the fuse queue', item)
+            data = {
+                "_msg": msg,
+                "message_type": "KojiTagChanged",
+                "item": item,
+                "item_type": ITEM_TYPE,
+                "tag": item,
+            }
 
             self.queued_tags.add(item)
+            self.queued_data[item] = data
         except exc.TriggerMsgError, e:
             self.log.debug(e)
diff --git a/jobtriggers/mongoquery.py b/jobtriggers/mongoquery.py
new file mode 100644
--- /dev/null
+++ b/jobtriggers/mongoquery.py
@@ -0,0 +1,299 @@
+# taken from https://github.com/kapouille/mongoquery
+# which is licensed as public domain
+# TODO: package mongoquery and remove the bundle. This is here for WIP purposes
+
+
+import re
+from collections import Sequence, Mapping
+
+try:
+    string_type = basestring
+except NameError:
+    string_type = str
+
+
+class QueryError(Exception):
+    pass
+
+
+class _Undefined(object):
+    pass
+
+
+def is_non_string_sequence(entry):
+    return isinstance(entry, Sequence) and not isinstance(entry, string_type)
+
+
+class Query(object):
+
+    def __init__(self, definition):
+        self._definition = definition
+
+    def match(self, entry):
+        return self._match(self._definition, entry)
+
+    def _match(self, condition, entry):
+        if isinstance(condition, Mapping):
+            return all(
+                self._process_condition(sub_operator, sub_condition, entry)
+                for sub_operator, sub_condition in condition.items()
+            )
+        else:
+            if is_non_string_sequence(entry):
+                return condition in entry
+            else:
+                return condition == entry
+
+    def _extract(self, entry, path):
+        if not path:
+            return entry
+        if entry is None:
+            return entry
+        if is_non_string_sequence(entry):
+            try:
+                index = int(path[0])
+                return self._extract(entry[index], path[1:])
+            except ValueError:
+                return [self._extract(item, path) for item in entry]
+        elif isinstance(entry, Mapping) and path[0] in entry:
+            return self._extract(entry[path[0]], path[1:])
+        else:
+            return _Undefined()
+
+    def _process_condition(self, operator, condition, entry):
+        if isinstance(condition, Mapping) and "$exists" in condition:
+            if condition["$exists"] != (operator in entry):
+                return False
+            elif tuple(condition.keys()) == ("$exists",):
+                return True
+        if isinstance(operator, string_type):
+            if operator.startswith("$"):
+                try:
+                    return getattr(self, "_" + operator[1:])(condition, entry)
+                except AttributeError:
+                    raise QueryError(
+                        "{!r} operator isn't supported".format(operator))
+            else:
+                extracted_data = self._extract(entry, operator.split("."))
+        else:
+            if operator not in entry:
+                return False
+            extracted_data = entry[operator]
+        return self._match(condition, extracted_data)
+
+    ##################
+    # Common operators
+    ##################
+
+    def _not_implemented(self, *args):
+        raise NotImplementedError
+
+    def _noop(self, *args):
+        return True
+
+    ######################
+    # Comparison operators
+    ######################
+
+    def _gt(self, condition, entry):
+        return entry > condition
+
+    def _gte(self, condition, entry):
+        return entry >= condition
+
+    def _in(self, condition, entry):
+        return entry in condition
+
+    def _lt(self, condition, entry):
+        return entry < condition
+
+    def _lte(self, condition, entry):
+        return entry <= condition
+
+    def _ne(self, condition, entry):
+        return entry != condition
+
+    def _nin(self, condition, entry):
+        return entry not in condition
+
+    ###################
+    # Logical operators
+    ###################
+
+    def _and(self, condition, entry):
+        if isinstance(condition, Sequence):
+            return all(
+                self._match(sub_condition, entry)
+                for sub_condition in condition
+            )
+        raise QueryError(
+            "$and has been attributed incorrect argument {!r}".format(
+                condition
+            )
+        )
+
+    def _nor(self, condition, entry):
+        if isinstance(condition, Sequence):
+            return all(
+                not self._match(sub_condition, entry)
+                for sub_condition in condition
+            )
+        raise QueryError(
+            "$nor has been attributed incorrect argument {!r}".format(
+                condition
+            )
+        )
+
+    def _not(self, condition, entry):
+        return not self._match(condition, entry)
+
+    def _or(self, condition, entry):
+        if isinstance(condition, Sequence):
+            return any(
+                self._match(sub_condition, entry)
+                for sub_condition in condition
+            )
+        raise QueryError(
+            "$nor has been attributed incorrect argument {!r}".format(
+                condition
+            )
+        )
+
+    ###################
+    # Element operators
+    ###################
+
+    def _type(self, condition, entry):
+        # TODO: further validation to ensure the right type
+        # rather than just checking
+        bson_type = {
+            1: float,
+            2: string_type,
+            3: Mapping,
+            4: Sequence,
+            5: bytearray,
+            7: string_type,  # object id (uuid)
+            8: bool,
+            9: string_type,  # date (UTC datetime)
+            10: type(None),
+            11: string_type,  # regex,
+            13: string_type,  # Javascript
+            15: string_type,  # JavaScript (with scope)
+            16: int,  # 32-bit integer
+            17: int,  # Timestamp
+            18: int   # 64-bit integer
+        }
+        bson_alias = {
+            "double": 1,
+            "string": 2,
+            "object": 3,
+            "array": 4,
+            "binData": 5,
+            "objectId": 7,
+            "bool": 8,
+            "date": 9,
+            "null": 10,
+            "regex": 11,
+            "javascript": 13,
+            "javascriptWithScope": 15,
+            "int": 16,
+            "timestamp": 17,
+            "long": 18,
+        }
+
+        if condition == "number":
+            return any([
+                isinstance(entry, bson_type[bson_alias[alias]])
+                for alias in ["double", "int", "long"]
+            ])
+
+        # resolves bson alias, or keeps original condition value
+        condition = bson_alias.get(condition, condition)
+
+        if condition not in bson_type:
+            raise QueryError(
+                "$type has been used with unknown type {!r}".format(condition))
+
+        return isinstance(entry, bson_type.get(condition))
+
+    _exists = _noop
+
+    ######################
+    # Evaluation operators
+    ######################
+
+    def _mod(self, condition, entry):
+        return entry % condition[0] == condition[1]
+
+    def _regex(self, condition, entry):
+        if not isinstance(entry, string_type):
+            return False
+        try:
+            regex = re.match(
+                "\A/(.+)/([imsx]{,4})\Z",
+                condition,
+                flags=re.DOTALL
+            )
+        except TypeError:
+            raise QueryError(
+                "{!r} is not a regular expression "
+                "and should be a string".format(condition))
+
+        if regex:
+            flags = 0
+            options = regex.group(2)
+            for option in options:
+                flags |= getattr(re, option.upper())
+            try:
+                match = re.search(regex.group(1), entry, flags=flags)
+            except Exception as error:
+                raise QueryError(
+                    "{!r} failed to execute with error {!r}".format(
+                        condition, error))
+            return bool(match)
+        else:
+            raise QueryError(
+                "{!r} is not using a known regular expression syntax".format(
+                    condition
+                )
+            )
+
+    _options = _text = _where = _not_implemented
+
+    #################
+    # Array operators
+    #################
+
+    def _all(self, condition, entry):
+        return all(
+            self._match(item, entry)
+            for item in condition
+        )
+
+    def _elemMatch(self, condition, entry):
+        return any(
+            all(
+                self._process_condition(sub_operator, sub_condition, element)
+                for sub_operator, sub_condition in condition.items()
+            )
+            for element in entry
+        )
+
+    def _size(self, condition, entry):
+        if not isinstance(condition, int):
+            raise QueryError(
+                "$size has been attributed incorrect argument {!r}".format(
+                    condition
+                )
+            )
+
+        if is_non_string_sequence(entry):
+            return len(entry) == condition
+
+        return False
+
+    ####################
+    # Comments operators
+    ####################
+
+    _comment = _noop
diff --git a/jobtriggers/mongoquery_string_template.py b/jobtriggers/mongoquery_string_template.py
new file mode 100644
--- /dev/null
+++ b/jobtriggers/mongoquery_string_template.py
@@ -0,0 +1,125 @@
+# The MongoTemplate is based on the Python 2.7's string library.
+#  Sadly the string.Template can not be easily modified to this
+#  extent without actually copying the actual code of substutite
+#  and safe_substitute, because we need to add another group to
+#  the pattern, and the methods dealing with it are encapsulated
+#  in the two afore-mentioned.
+
+import string
+
+
+class _multimap:
+
+    """Helper class for combining multiple mappings.
+
+    Used by .{safe_,}substitute() to combine the mapping and keyword
+    arguments.
+    """
+
+    def __init__(self, primary, secondary):
+        self._primary = primary
+        self._secondary = secondary
+
+    def __getitem__(self, key):
+        try:
+            return self._primary[key]
+        except KeyError:
+            return self._secondary[key]
+
+
+class MongoTemplate(string.Template):
+
+    """Makes sure the mongo keywords ($and, $or, ...) don't get replaced.
+    This will still replace ${and} ${or} though, and that is expected
+    """
+    __mongo_operators = [
+        'exists',
+        # comparison operators
+        'gt', 'gte', 'in', 'lt', 'lte', 'ne', 'nin',
+        # logical operators
+        'and', 'nor', 'not', 'or',
+        # element operators
+        'type',
+        # evaluation operators
+        'mod', 'regex', 'options', 'text', 'where',
+        # array operators
+        'all', 'elemMatch', 'size',
+        # comment operators
+        'comment',
+    ]
+
+    pattern = r"""
+    \$(?:
+      (?P<escaped>\$) |   # Escape sequence of two delimiters
+      (?P<mongo>(?:%s)\W) | # mongo keywords to 'ignore'
+      (?P<named>[_a-z][_a-z0-9]*) |
+      {(?P<braced>[_a-z][_a-z0-9]*)}   |   # delimiter and a braced identifier
+      (?P<invalid>)              # Other ill-formed delimiter exprs
+    )""" % '|'.join(__mongo_operators)
+
+    def substitute(*args, **kws):
+        if not args:
+            raise TypeError("descriptor 'substitute' of 'Template' object "
+                            "needs an argument")
+        self, args = args[0], args[1:]  # allow the "self" keyword be passed
+        if len(args) > 1:
+            raise TypeError('Too many positional arguments')
+        if not args:
+            mapping = kws
+        elif kws:
+            mapping = _multimap(kws, args[0])
+        else:
+            mapping = args[0]
+        # Helper function for .sub()
+
+        def convert(mo):
+            # Check the most common path first.
+            named = mo.group('named') or mo.group('braced')
+            if named is not None:
+                val = mapping[named]
+                # We use this idiom instead of str() because the latter will
+                # fail if val is a Unicode containing non-ASCII characters.
+                return '%s' % (val,)
+            if mo.group('escaped') is not None:
+                return self.delimiter
+            if mo.group('mongo') is not None:
+                return "$%s" % mo.group('mongo')
+            if mo.group('invalid') is not None:
+                self._invalid(mo)
+            raise ValueError('Unrecognized XXX named group in pattern',
+                             self.pattern)
+        return self.pattern.sub(convert, self.template)
+
+    def safe_substitute(*args, **kws):
+        if not args:
+            raise TypeError("descriptor 'safe_substitute' of 'Template' object "
+                            "needs an argument")
+        self, args = args[0], args[1:]  # allow the "self" keyword be passed
+        if len(args) > 1:
+            raise TypeError('Too many positional arguments')
+        if not args:
+            mapping = kws
+        elif kws:
+            mapping = _multimap(kws, args[0])
+        else:
+            mapping = args[0]
+        # Helper function for .sub()
+
+        def convert(mo):
+            named = mo.group('named') or mo.group('braced')
+            if named is not None:
+                try:
+                    # We use this idiom instead of str() because the latter
+                    # will fail if val is a Unicode containing non-ASCII
+                    return '%s' % (mapping[named],)
+                except KeyError:
+                    return mo.group()
+            if mo.group('escaped') is not None:
+                return self.delimiter
+            if mo.group('mongo') is not None:
+                return "$%s" % mo.group('mongo')
+            if mo.group('invalid') is not None:
+                return mo.group()
+            raise ValueError('Unrecognized named group in pattern',
+                             self.pattern)
+        return self.pattern.sub(convert, self.template)
diff --git a/jobtriggers/triggers.py b/jobtriggers/runners.py
rename from jobtriggers/triggers.py
rename to jobtriggers/runners.py
--- a/jobtriggers/triggers.py
+++ b/jobtriggers/runners.py
@@ -2,13 +2,11 @@
 import json
 
 from . import config
+from .exceptions import RunnerException
 
 
-class TriggerException(Exception):
-    pass
+class BaseRunner(object):
 
-
-class BaseTrigger(object):
     """
     Format:
     {
@@ -18,13 +16,15 @@
         'arch': <arch needed for execution, x86_64, i386 etc.>
     }
     """
+
     def trigger_job(self, item, item_type, taskname, arch, **kwargs):
         raise NotImplementedError
 
 
-class StreamTrigger(BaseTrigger):
+class StreamRunner(BaseRunner):
+
     def trigger_job(self, item, item_type, taskname, arch, **kwargs):
-        output = ["StreamTrigger received:",
+        output = ["StreamRunner received:",
                   "item: %s" % item,
                   "item_type: %s" % item_type,
                   "taskname: %s" % taskname,
@@ -34,7 +34,8 @@
         yield '\n'.join(output)
 
 
-class BuildbotTrigger(BaseTrigger):
+class BuildbotRunner(BaseRunner):
+
     def trigger_job(self, item, item_type, taskname, arch, **kwargs):
         output = ["Buildbot received:",
                   "item: %s" % item,
@@ -55,19 +56,19 @@
         try:
             # Create new job in ExecDB
             req_uuid = requests.post("%s/jobs" % config.execdb_server,
-                                headers={'Content-type': 'application/json',
-                                        'Accept': 'text/plain'},
-                                data=json.dumps(properties))
+                                     headers={'Content-type': 'application/json',
+                                              'Accept': 'text/plain'},
+                                     data=json.dumps(properties))
 
             if req_uuid.status_code != 201:
-                raise TriggerException("ExecDB - bad status code: %s" % req_uuid.status_code)
+                raise RunnerException("ExecDB - bad status code: %s" % req_uuid.status_code)
             else:
                 uuid = req_uuid.json()['uuid']
 
         except requests.exceptions.ConnectionError:
             yield "error: connection to ExecDB failed; %r" % properties
 
-        except TriggerException as e:
+        except RunnerException as e:
             yield "error: %s; %r" % (e, properties)
 
         else:
@@ -84,8 +85,8 @@
             yield "post-hook build request status: %s" % str(r.status_code)
 
 
-_triggers = {'StreamTrigger': StreamTrigger, 'BuildbotTrigger': BuildbotTrigger}
+_runners = {'StreamRunner': StreamRunner, 'BuildbotRunner': BuildbotRunner}
 
 
-def get_trigger(trigger_type):
-    return _triggers[trigger_type]()
+def get_runner(runner_type):
+    return _runners[runner_type]()
diff --git a/jobtriggers/utils.py b/jobtriggers/utils.py
--- a/jobtriggers/utils.py
+++ b/jobtriggers/utils.py
@@ -27,7 +27,7 @@
             raise exc.TriggerError(e)
 
 
-def should_schedule(formula_file, item_type):
+def _should_schedule(formula_file, item_type):
     formula = parse_yaml_from_file(formula_file)
     try:
         run = formula['scheduling']['run']
@@ -37,20 +37,20 @@
     return run and item_type in formula['input']['args']
 
 
-def get_branch(release):
+def get_distgit_branch(release):
     matches = re.findall(r'\.(fc\d{1,2})\.?', release)
     if not matches:
         raise exc.TriggerMsgError('Could not parse disttag from %s' % release)
 
     return matches[-1].replace('c', '')
 
 
-def get_repo(pkgname):
-    return '/'.join([config.distgit_repo_url, config.distgit_namespace, '%s.git' % pkgname])
+def discover_tasks(item_type, repo, branch='master'):
+    dirname = repo.split('/')[-1]
+    if dirname.endswith('.git'):
+        dirname = dirname[:-len(".git")]
 
-
-def get_distgit_tasks(pkgname, item_type, repo, branch='master'):
-    tasks_dir = os.path.join(config.distgit_cache_dir, pkgname)
+    tasks_dir = os.path.join(config.git_cache_dir, dirname)
 
     # for now, just delete the repo and make a new shallow copy
     if os.path.exists(tasks_dir):
@@ -61,17 +61,19 @@
     try:
         subprocess.check_output(cmd, stderr=subprocess.STDOUT)
     except subprocess.CalledProcessError as e:
+        # FIXME: this does not really work, since the change in branch is not propagated
+        # back, but leaving here for completeness for now
         if 'Could not find remote branch' in e.output and branch != 'master':
-            return get_distgit_tasks(pkgname, item_type, repo, branch='master')
+            return discover_tasks(item_type, repo, branch='master')
         raise exc.TriggerError(e.output)
 
     tasks = []
     for task in os.listdir(tasks_dir):
         if task.startswith('.'):
             continue
         taskdir = os.path.join(tasks_dir, task)
         formula_file = os.path.join(taskdir, 'runtask.yml')
-        if os.path.isdir(taskdir) and should_schedule(formula_file, item_type):
+        if os.path.isdir(taskdir) and _should_schedule(formula_file, item_type):
             tasks.append(task)
 
     return tasks
diff --git a/testing/conftest.py b/testing/conftest.py
--- a/testing/conftest.py
+++ b/testing/conftest.py
@@ -19,6 +19,7 @@
 '''py.test configuration and plugins
 Read more at: http://pytest.org/latest/plugins.html#conftest-py-plugins'''
 
+
 def pytest_addoption(parser):
     """
     Add an option to the py.test parser to detect when the functional tests
diff --git a/testing/functest_cli.py b/testing/functest_cli.py
--- a/testing/functest_cli.py
+++ b/testing/functest_cli.py
@@ -5,6 +5,7 @@
 
 
 class TestCli():
+
     def setup_method(self, method):
         self.ref_tests = ['rpmlint', 'upgradepath']
         self.ref_unit_types = ['koji_build', 'bodhi_id']
@@ -15,16 +16,20 @@
         f = tmpdir.join('jobs.csv')
         filename = str(f.realpath())
 
-        monkeypatch.setattr(jobtriggers.jobrunner, 'trigger', Dingus())
+        monkeypatch.setattr(jobtriggers.jobrunner, 'runner', Dingus())
 
-        log_job(filename, self.ref_tests[0], self.ref_unit_types[0], self.ref_nvr, self.ref_arch)
-        log_job(filename, self.ref_tests[1], self.ref_unit_types[1], self.ref_nvr, self.ref_arch)
+        log_job(filename, self.ref_tests[0], self.ref_unit_types[
+                0], self.ref_nvr, self.ref_arch)
+        log_job(filename, self.ref_tests[1], self.ref_unit_types[
+                1], self.ref_nvr, self.ref_arch)
 
         jobtriggers.jobrunner.file_jobs(filename)
 
-        trigger_calls = jobtriggers.jobrunner.trigger.trigger_job.calls()
+        runner_calls = jobtriggers.jobrunner.runner.trigger_job.calls()
 
-        assert len(trigger_calls) == 2
+        assert len(runner_calls) == 2
 
-        assert trigger_calls[0][1] == (self.ref_tests[0], self.ref_unit_types[0], self.ref_nvr, self.ref_arch)
-        assert trigger_calls[1][1] == (self.ref_tests[1], self.ref_unit_types[1], self.ref_nvr, self.ref_arch)
+        assert runner_calls[0][1] == (
+            self.ref_tests[0], self.ref_unit_types[0], self.ref_nvr, self.ref_arch)
+        assert runner_calls[1][1] == (
+            self.ref_tests[1], self.ref_unit_types[1], self.ref_nvr, self.ref_arch)
diff --git a/testing/test_compose_trigger.py b/testing/test_compose_trigger.py
--- a/testing/test_compose_trigger.py
+++ b/testing/test_compose_trigger.py
@@ -2,11 +2,13 @@
 from dingus import Dingus
 from munch import Munch
 
+from jobtriggers import jobtrigger
 from jobtriggers import compose_complete_msg
 
 
 @pytest.mark.usefixtures('prepare')
 class TestComposeCompletedJobTrigger():
+
     @pytest.fixture
     def prepare(self, monkeypatch):
         self.ref_arch = ''
@@ -16,63 +18,72 @@
         self._create_msg(self.ref_arch, self.ref_branch, self.ref_topic)
 
         self.ref_tasks = ['compose_task1', 'compose_task2']
-        self.ref_arches = 'noarch'
+        self.ref_arches = 'x86_64'
 
         stub_hub = Munch(config=Munch(get=0))
 
         self.helper = compose_complete_msg.ComposeCompletedJobTrigger(stub_hub)
-        self.helper.trigger = Dingus()
+        self.helper.runner = Dingus()
+
+        jobtrigger.config.compose_completed_tasks = self.ref_tasks
+        jobtrigger.config.valid_arches = [self.ref_arches]
+        jobtrigger.config.job_logging = False
 
-        compose_complete_msg.config.compose_completed_tasks = self.ref_tasks
-        compose_complete_msg.config.valid_arches = self.ref_arches
-        compose_complete_msg.config.job_logging = False
+        jobtrigger.config.trigger_rules_template = """---
+- do:
+  - {tasks: %r}
+  when: {message_type: ComposeCompleted}
+""" % (self.ref_tasks)
 
     def _create_msg(self, ref_arch, ref_branch, ref_topic):
         self.ref_message = Munch(body='{"i": 1,\
                                   "msg": {"arch": "%s",\
                                           "branch": "%s",\
                                           "log": "done"},\
                                   "topic": "%s"}' %
-                                  (ref_arch, ref_branch, ref_topic))
+                                 (ref_arch, ref_branch, ref_topic))
 
     def test_consume(self):
         self.helper.consume(self.ref_message)
 
-        trigger_calls = self.helper.trigger.trigger_job.calls()
+        runner_calls = self.helper.runner.trigger_job.calls()
 
-        assert len(trigger_calls) == 2
+        assert len(runner_calls) == 2
 
-        assert trigger_calls[0][1] == (self.ref_branch, compose_complete_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_arches)
-        assert trigger_calls[1][1] == (self.ref_branch, compose_complete_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_arches)
+        assert runner_calls[0][1] == (
+            self.ref_branch, compose_complete_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_arches)
+        assert runner_calls[1][1] == (
+            self.ref_branch, compose_complete_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_arches)
 
     def test_consume_secondary_arch(self):
         ref_arch = 's390'
         self._create_msg(ref_arch, self.ref_branch, self.ref_topic)
 
         self.helper.consume(self.ref_message)
 
-        trigger_calls = self.helper.trigger.trigger_job.calls()
+        runner_calls = self.helper.runner.trigger_job.calls()
 
-        assert len(trigger_calls) == 0
+        assert len(runner_calls) == 0
 
     def test_consume_incorrect_topic(self):
         ref_topic = 'fake_topic'
         self._create_msg(self.ref_arch, self.ref_branch, ref_topic)
 
         self.helper.consume(self.ref_message)
 
-        trigger_calls = self.helper.trigger.trigger_job.calls()
+        runner_calls = self.helper.runner.trigger_job.calls()
 
-        assert len(trigger_calls) == 0
+        assert len(runner_calls) == 0
 
     def test_consume_allowed_topics(self):
-        ref_topics = ['org.fedoraproject.prod.compose.branched.complete', 'org.fedoraproject.prod.compose.rawhide.complete']
+        ref_topics = ['org.fedoraproject.prod.compose.branched.complete',
+                      'org.fedoraproject.prod.compose.rawhide.complete']
 
         for ref_topic in ref_topics:
             self._create_msg(self.ref_arch, self.ref_branch, ref_topic)
 
             self.helper.consume(self.ref_message)
 
-            trigger_calls = self.helper.trigger.trigger_job.calls()
+            runner_calls = self.helper.runner.trigger_job.calls()
 
-        assert len(trigger_calls) == 4
+        assert len(runner_calls) == 4
diff --git a/testing/test_jobtrigger.py b/testing/test_jobtrigger.py
new file mode 100644
--- /dev/null
+++ b/testing/test_jobtrigger.py
@@ -0,0 +1,150 @@
+import pytest
+import mock
+import copy
+from dingus import Dingus
+from munch import Munch
+
+from jobtriggers import jobtrigger
+from jobtriggers import utils
+from jobtriggers import exceptions as exc
+
+
+class BogusJobTrigger(jobtrigger.JobTrigger):
+    config_key = 'taskotron.bogusjobtrigger.enabled'
+
+
+@pytest.mark.usefixtures('prepare')
+class TestJobtrigger():
+
+    @pytest.fixture
+    def prepare(self, monkeypatch):
+        stub_hub = Munch(config=Munch(get=0))
+        self.helper = BogusJobTrigger(stub_hub)
+        self.helper.runner = Dingus()
+
+        self.ref_discovered_tasks = ['discovered_task_1']
+        mock_discover_tasks = mock.Mock(return_value=self.ref_discovered_tasks)
+        monkeypatch.setattr(utils, 'discover_tasks', mock_discover_tasks)
+
+        self.ref_item = "package_foobar-1.0.8-fc18"
+        self.ref_item_type = "bar"
+        self.ref_name = "package_foobar"
+        self.ref_message_data = {
+            "item": self.ref_item, "item_type": self.ref_item_type, "name": self.ref_name}
+        self.ref_default_arches = ['i386', 'x86_64']
+
+        self.ref_tasks = ['ref_task1', 'ref_task2']
+        self.ref_rules = [
+            {'do': [{'discover': {'repo': 'http://bogus.repo/${name}.git'}}],
+             'when': {'message_type': 'DiscoverTasks'}},
+            {'do': [{'tasks': self.ref_tasks}],
+             'when': {'message_type': 'RunTasks'}},
+            {'do': [{'tasks': self.ref_tasks}],
+             'when': {'message_type': 'DisabledTasks'},
+             'enabled': False},
+        ]
+
+        jobtrigger.config.valid_arches = self.ref_default_arches
+
+    def test_load_rules(self):
+        template = """$in: [${foo}, ${in}]"""
+        data = {"foo": "bar", "in": "out"}
+
+        assert self.helper._load_rules(
+            template, {}) == {"$in": ["MISSING_VAR_foo", "MISSING_VAR_in"]}
+        assert self.helper._load_rules(
+            template, data) == {"$in": ['bar', 'out']}
+
+    def test__tasks_from_rule_exceptions(self):
+        with pytest.raises(exc.TriggerMsgError):
+            self.helper._tasks_from_rule(None, {})
+        with pytest.raises(exc.TriggerMsgError):
+            self.helper._tasks_from_rule(None, {'item': 'foo'})
+        with pytest.raises(exc.TriggerMsgError):
+            self.helper._tasks_from_rule(None, {'item_type': 'bar'})
+
+    def test__tasks_from_rule(self):
+        rule = {'do': [{'tasks': self.ref_tasks}]}
+        tasks = self.helper._tasks_from_rule(rule, self.ref_message_data)
+        assert len(tasks) == 1
+        assert tasks[0]['tasks'] == self.ref_tasks
+        assert tasks[0]['item'] == self.ref_message_data['item']
+        assert tasks[0]['item_type'] == self.ref_message_data['item_type']
+        assert tasks[0]['arches'] == self.ref_default_arches
+
+    def test__tasks_from_rule_discover(self):
+        ref_repo = 'http://bogus.repo/firefox.git'
+        rule = {'do': [{'discover': {'repo': ref_repo}}]}
+        tasks = self.helper._tasks_from_rule(rule, self.ref_message_data)
+        assert len(tasks) == 1
+        assert tasks[0]['tasks'] == self.ref_discovered_tasks
+        assert tasks[0]['item'] == self.ref_message_data['item']
+        assert tasks[0]['item_type'] == self.ref_message_data['item_type']
+        assert tasks[0]['repo'] == ref_repo
+        assert tasks[0]['branch'] == 'master'
+
+    def test__tasks_from_rule_discover_set_branch(self):
+        ref_repo = 'http://bogus.repo/firefox.git'
+        rule = {'do': [{'discover': {'repo': ref_repo, 'branch': 'mybranch'}}]}
+        tasks = self.helper._tasks_from_rule(rule, self.ref_message_data)
+        assert len(tasks) == 1
+        assert tasks[0]['tasks'] == self.ref_discovered_tasks
+        assert tasks[0]['item'] == self.ref_message_data['item']
+        assert tasks[0]['item_type'] == self.ref_message_data['item_type']
+        assert tasks[0]['repo'] == ref_repo
+        assert tasks[0]['branch'] == 'mybranch'
+
+    def test_do_trigger_runtasks(self, monkeypatch):
+        mock__load_rules = mock.Mock(return_value=self.ref_rules)
+        monkeypatch.setattr(self.helper, '_load_rules', mock__load_rules)
+
+        message_data = copy.deepcopy(self.ref_message_data)
+        message_data['message_type'] = 'RunTasks'
+
+        self.helper.do_trigger(message_data)
+
+        runner_calls = self.helper.runner.trigger_job.calls()
+
+        assert len(runner_calls) == 4
+
+        assert runner_calls[0][1] == (
+            self.ref_item, self.ref_item_type, self.ref_tasks[0], self.ref_default_arches[0])
+        assert runner_calls[1][1] == (
+            self.ref_item, self.ref_item_type, self.ref_tasks[0], self.ref_default_arches[1])
+        assert runner_calls[2][1] == (
+            self.ref_item, self.ref_item_type, self.ref_tasks[1], self.ref_default_arches[0])
+        assert runner_calls[3][1] == (
+            self.ref_item, self.ref_item_type, self.ref_tasks[1], self.ref_default_arches[1])
+
+    def test_do_trigger_discover(self, monkeypatch):
+        mock__load_rules = mock.Mock(return_value=self.ref_rules)
+        monkeypatch.setattr(self.helper, '_load_rules', mock__load_rules)
+
+        message_data = copy.deepcopy(self.ref_message_data)
+        message_data['message_type'] = 'DiscoverTasks'
+
+        self.helper.do_trigger(message_data)
+
+        runner_calls = self.helper.runner.trigger_job.calls()
+
+        assert len(runner_calls) == 2
+
+        assert runner_calls[0][1] == (
+            self.ref_item, self.ref_item_type, self.ref_discovered_tasks[0],
+            self.ref_default_arches[0])
+        assert runner_calls[1][1] == (
+            self.ref_item, self.ref_item_type, self.ref_discovered_tasks[0],
+            self.ref_default_arches[1])
+
+    def test_do_trigger_disabled(self, monkeypatch):
+        mock__load_rules = mock.Mock(return_value=self.ref_rules)
+        monkeypatch.setattr(self.helper, '_load_rules', mock__load_rules)
+
+        message_data = copy.deepcopy(self.ref_message_data)
+        message_data['message_type'] = 'DisabledTasks'
+
+        self.helper.do_trigger(message_data)
+
+        runner_calls = self.helper.runner.trigger_job.calls()
+
+        assert len(runner_calls) == 0
diff --git a/testing/test_koji_build_trigger.py b/testing/test_koji_build_trigger.py
--- a/testing/test_koji_build_trigger.py
+++ b/testing/test_koji_build_trigger.py
@@ -10,131 +10,164 @@
 
 @pytest.mark.usefixtures('prepare')
 class TestKojiBuildCompletedJobTrigger():
+
     @pytest.fixture
     def prepare(self, monkeypatch):
         self.ref_instance = 'primary'
         self.ref_new = koji.BUILD_STATES['COMPLETE']
         self.ref_name = 'stage'
         self.ref_version = '4.1.1'
         self.ref_release = '3.fc18'
-        self.ref_nvr = '%s-%s-%s' % (self.ref_name, self.ref_version, self.ref_release)
-        self.ref_nvr_docker = '%s-%s-%s' % ('docker', self.ref_version, self.ref_release)
+        self.ref_nvr = '%s-%s-%s' % (self.ref_name,
+                                     self.ref_version, self.ref_release)
+        self.ref_nvr_docker = '%s-%s-%s' % ('docker',
+                                            self.ref_version, self.ref_release)
 
-        self._create_msg(self.ref_instance, self.ref_new, self.ref_name, self.ref_release, self.ref_version)
+        self._create_msg(
+            self.ref_instance, self.ref_new, self.ref_name, self.ref_release, self.ref_version)
 
         self.ref_tasks = ['koji_build_task1', 'koji_build_task2']
         self.ref_tasks_docker = ['docker_build_task']
         self.ref_validarches = ['i386', 'x86_64']
 
         stub_hub = Munch(config=Munch(get=0))
 
         self.helper = koji_build_msg.KojiBuildCompletedJobTrigger(stub_hub)
-        self.helper.trigger = Dingus()
+        self.helper.runner = Dingus()
+
+        koji_build_msg.config.trigger_rules_template = """---
+- do:
+  - {discover: {repo: 'http://bogus/firefox.git'}}
+  - {tasks: %s}
+  when: {message_type: KojiBuildCompleted}
+- do:
+  - {tasks: %s}
+  when: {message_type: KojiBuildCompleted, name: docker}
+""" % (self.ref_tasks, self.ref_tasks_docker)
 
-        koji_build_msg.config.koji_build_completed_tasks = self.ref_tasks
-        koji_build_msg.config.docker_build_tasks = self.ref_tasks_docker
         koji_build_msg.config.valid_arches = self.ref_validarches
         koji_build_msg.config.job_logging = False
 
-        mock_get_distgit_tasks = mock.Mock(return_value=[])
-        monkeypatch.setattr(utils, 'get_distgit_tasks', mock_get_distgit_tasks)
+        mock_discover_tasks = mock.Mock(return_value=[])
+        monkeypatch.setattr(utils, 'discover_tasks', mock_discover_tasks)
 
         mock_critpath = {"pkgs": {"f18": [], "master": []}}
         mock_get_critpath = mock.Mock(return_value=mock_critpath)
         monkeypatch.setattr(utils, 'parse_yaml_from_file', mock_get_critpath)
 
     def _create_msg(self, ref_instance, ref_new, ref_name, ref_release, ref_version):
-        self.ref_message = Munch(body='{"i": 1,\
-                                        "msg": {"build_id": 221146,\
-                                                "instance": "%s",\
-                                                "new": %d,\
-                                                "name": "%s",\
-                                                "release": "%s",\
-                                                "version": "%s"},\
-                                        "timestamp": 1359603469.21164,\
-                                        "topic": "org.fedoraproject.prod.buildsys.build.state.change",\
-                                        "username": "apache"}' %
-                                        (ref_instance, ref_new, ref_name, ref_release, ref_version))
+        msg = Munch(body='{"i": 1,\
+                           "msg": {"build_id": 221146,\
+                                   "instance": "%s",\
+                                   "new": %d,\
+                                   "name": "%s",\
+                                   "release": "%s",\
+                                   "version": "%s"},\
+                           "timestamp": 1359603469.21164,\
+                           "topic": "org.fedoraproject.prod.buildsys.build.state.change",\
+                           "username": "apache"}' %
+                    (ref_instance, ref_new, ref_name, ref_release, ref_version))
+        self.ref_message = msg
 
     def test_consume(self):
         self.helper.consume(self.ref_message)
 
-        trigger_calls = self.helper.trigger.trigger_job.calls()
+        runner_calls = self.helper.runner.trigger_job.calls()
 
-        assert len(trigger_calls) == 4
+        assert len(runner_calls) == 4
 
-        assert trigger_calls[0][1] == (self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[0])
-        assert trigger_calls[1][1] == (self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[1])
-        assert trigger_calls[2][1] == (self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_validarches[0])
-        assert trigger_calls[3][1] == (self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_validarches[1])
+        assert runner_calls[0][1] == (
+            self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[0])
+        assert runner_calls[1][1] == (
+            self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[1])
+        assert runner_calls[2][1] == (
+            self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_validarches[0])
+        assert runner_calls[3][1] == (
+            self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_validarches[1])
 
     def test_consume_docker(self):
         ref_name = 'docker'
-        self._create_msg(self.ref_instance, self.ref_new, ref_name, self.ref_release, self.ref_version)
+        self._create_msg(
+            self.ref_instance, self.ref_new, ref_name, self.ref_release, self.ref_version)
 
         self.helper.consume(self.ref_message)
 
-        trigger_calls = self.helper.trigger.trigger_job.calls()
-
-        assert len(trigger_calls) == 6
-
-        assert trigger_calls[0][1] == (self.ref_nvr_docker, koji_build_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[0])
-        assert trigger_calls[1][1] == (self.ref_nvr_docker, koji_build_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[1])
-        assert trigger_calls[2][1] == (self.ref_nvr_docker, koji_build_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_validarches[0])
-        assert trigger_calls[3][1] == (self.ref_nvr_docker, koji_build_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_validarches[1])
-        assert trigger_calls[4][1] == (self.ref_nvr_docker, koji_build_msg.ITEM_TYPE, self.ref_tasks_docker[0], self.ref_validarches[0])
-        assert trigger_calls[5][1] == (self.ref_nvr_docker, koji_build_msg.ITEM_TYPE, self.ref_tasks_docker[0], self.ref_validarches[1])
+        runner_calls = self.helper.runner.trigger_job.calls()
+
+        assert len(runner_calls) == 6
+
+        assert runner_calls[0][1] == (
+            self.ref_nvr_docker, koji_build_msg.ITEM_TYPE, self.ref_tasks[0],
+            self.ref_validarches[0])
+        assert runner_calls[1][1] == (
+            self.ref_nvr_docker, koji_build_msg.ITEM_TYPE, self.ref_tasks[0],
+            self.ref_validarches[1])
+        assert runner_calls[2][1] == (
+            self.ref_nvr_docker, koji_build_msg.ITEM_TYPE, self.ref_tasks[1],
+            self.ref_validarches[0])
+        assert runner_calls[3][1] == (
+            self.ref_nvr_docker, koji_build_msg.ITEM_TYPE, self.ref_tasks[1],
+            self.ref_validarches[1])
+        assert runner_calls[4][1] == (
+            self.ref_nvr_docker, koji_build_msg.ITEM_TYPE, self.ref_tasks_docker[0],
+            self.ref_validarches[0])
+        assert runner_calls[5][1] == (
+            self.ref_nvr_docker, koji_build_msg.ITEM_TYPE, self.ref_tasks_docker[0],
+            self.ref_validarches[1])
 
     def test_consume_distgit(self, monkeypatch):
         ref_distgit_task = 'abicheck'
-        mock_get_distgit_tasks = mock.Mock(return_value=[ref_distgit_task])
-        monkeypatch.setattr(utils, 'get_distgit_tasks', mock_get_distgit_tasks)
-
-        mock_critpath = {"pkgs": {"f18": [self.ref_name], "master": []}}
-        mock_get_critpath = mock.Mock(return_value=mock_critpath)
-        monkeypatch.setattr(utils, 'parse_yaml_from_file', mock_get_critpath)
+        mock_discover_tasks = mock.Mock(return_value=[ref_distgit_task])
+        monkeypatch.setattr(utils, 'discover_tasks', mock_discover_tasks)
 
         self.helper.consume(self.ref_message)
 
-        trigger_calls = self.helper.trigger.trigger_job.calls()
+        runner_calls = self.helper.runner.trigger_job.calls()
 
-        assert len(trigger_calls) == 8
+        assert len(runner_calls) == 6
 
-        assert trigger_calls[0][1] == (self.ref_nvr, koji_build_msg.ITEM_TYPE, ref_distgit_task, self.ref_validarches[0])
-        assert trigger_calls[1][1] == (self.ref_nvr, koji_build_msg.ITEM_TYPE, ref_distgit_task, self.ref_validarches[1])
-        assert trigger_calls[2][1] == (self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[0])
-        assert trigger_calls[3][1] == (self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[1])
-        assert trigger_calls[4][1] == (self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_validarches[0])
-        assert trigger_calls[5][1] == (self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_validarches[1])
-        assert trigger_calls[6][1] == (self.ref_nvr, koji_build_msg.ITEM_TYPE, ref_distgit_task, self.ref_validarches[0])
-        assert trigger_calls[7][1] == (self.ref_nvr, koji_build_msg.ITEM_TYPE, ref_distgit_task, self.ref_validarches[1])
+        assert runner_calls[0][1] == (
+            self.ref_nvr, koji_build_msg.ITEM_TYPE, ref_distgit_task, self.ref_validarches[0])
+        assert runner_calls[1][1] == (
+            self.ref_nvr, koji_build_msg.ITEM_TYPE, ref_distgit_task, self.ref_validarches[1])
+        assert runner_calls[2][1] == (
+            self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[0])
+        assert runner_calls[3][1] == (
+            self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[1])
+        assert runner_calls[4][1] == (
+            self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_validarches[0])
+        assert runner_calls[5][1] == (
+            self.ref_nvr, koji_build_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_validarches[1])
 
     def test_consume_not_primary_instance(self):
         ref_instance = 'ppc'
-        self._create_msg(ref_instance, self.ref_new, self.ref_name, self.ref_release, self.ref_version)
+        self._create_msg(
+            ref_instance, self.ref_new, self.ref_name, self.ref_release, self.ref_version)
 
         self.helper.consume(self.ref_message)
 
-        trigger_calls = self.helper.trigger.trigger_job.calls()
+        runner_calls = self.helper.runner.trigger_job.calls()
 
-        assert trigger_calls == []
+        assert runner_calls == []
 
     def test_consume_notcomplete_state(self):
         ref_new = koji.BUILD_STATES['BUILDING']
-        self._create_msg(self.ref_instance, ref_new, self.ref_name, self.ref_release, self.ref_version)
+        self._create_msg(
+            self.ref_instance, ref_new, self.ref_name, self.ref_release, self.ref_version)
 
         self.helper.consume(self.ref_message)
 
-        trigger_calls = self.helper.trigger.trigger_job.calls()
+        runner_calls = self.helper.runner.trigger_job.calls()
 
-        assert trigger_calls == []
+        assert runner_calls == []
 
     def test_consume_blacklisted_release(self):
         ref_release = '3.el5.1'
-        self._create_msg(self.ref_instance, self.ref_new, self.ref_name, ref_release, self.ref_version)
+        self._create_msg(
+            self.ref_instance, self.ref_new, self.ref_name, ref_release, self.ref_version)
 
         self.helper.consume(self.ref_message)
 
-        trigger_calls = self.helper.trigger.trigger_job.calls()
+        runner_calls = self.helper.runner.trigger_job.calls()
 
-        assert trigger_calls == []
+        assert runner_calls == []
diff --git a/testing/test_koji_tag_trigger.py b/testing/test_koji_tag_trigger.py
--- a/testing/test_koji_tag_trigger.py
+++ b/testing/test_koji_tag_trigger.py
@@ -1,32 +1,52 @@
 import pytest
 from dingus import Dingus
 from munch import Munch
+from copy import deepcopy
+import fedmsg.encoding
 
 from jobtriggers import koji_tag_msg
 
 
 @pytest.mark.usefixtures('prepare')
 class TestKojiTagChangedJobTrigger():
+
     @pytest.fixture
     def prepare(self, monkeypatch):
         self.ref_instance = 'primary'
         self.ref_name = 'stage'
         self.ref_version = '4.1.1'
         self.ref_release = '3.fc18'
         self.ref_tag = 'f18-updates-pending'
-        self.ref_nvr = '%s-%s-%s' % (self.ref_name, self.ref_version, self.ref_release)
+        self.ref_nvr = '%s-%s-%s' % (self.ref_name,
+                                     self.ref_version, self.ref_release)
 
         self.ref_tasks = ['depcheck', 'upgradepath']
         self.ref_validarches = ['i386', 'x86_64']
 
-        self._create_msg(self.ref_instance, self.ref_name, self.ref_release, self.ref_tag, self.ref_version)
+        self._create_msg(
+            self.ref_instance, self.ref_name, self.ref_release, self.ref_tag, self.ref_version)
+
+        self.ref_data = {
+            "_msg": {},
+            "message_type": "KojiTagChanged",
+            "item": self.ref_tag,
+            "item_type": "koji_tag",
+                         "tag": self.ref_tag,
+        }
 
         stub_hub = Munch(config=Munch(get=0))
         self.helper = koji_tag_msg.KojiTagChangedJobTrigger(stub_hub)
 
-        self.helper.trigger = Dingus()
+        self.helper.runner = Dingus()
+        koji_tag_msg.config.trigger_rules_template = """---
+- do:
+  - {tasks: [depcheck]}
+  when: {message_type: KojiTagChanged}
+- do:
+  - {tasks: [upgradepath]}
+  when: {message_type: KojiTagChanged, tag: {$regex: '/.*(?<!testing-pending)$$/'}}
+"""
 
-        koji_tag_msg.config.koji_tag_changed_tasks = self.ref_tasks
         koji_tag_msg.config.valid_arches = self.ref_validarches
         koji_tag_msg.config.job_logging = False
 
@@ -44,74 +64,97 @@
                                         "timestamp": 1359603469.21164,\
                                         "topic": "org.fedoraproject.prod.buildsys.tag",\
                                         "username": "apache"}' %
-                                        (ref_instance, ref_name, ref_release, ref_tag, ref_version))
+                                 (ref_instance, ref_name, ref_release, ref_tag, ref_version))
 
     def test_delayed_queue(self):
         self.helper.consume(self.ref_message)
 
         assert self.helper.queued_tags == set([self.ref_tag])
+        assert self.helper.queued_data[self.ref_tag] == {
+            "_msg": fedmsg.encoding.loads(self.ref_message.body),
+            "message_type": "KojiTagChanged",
+            "item": self.ref_tag,
+            "item_type": "koji_tag",
+            "tag": self.ref_tag,
+        }
 
     def test_delayed_consume(self):
         self.helper.queued_tags = [self.ref_tag]
+        self.helper.queued_data = {self.ref_tag: self.ref_data}
 
         self.helper.delayed_consume()
 
-        trigger_calls = self.helper.trigger.trigger_job.calls()
+        runner_calls = self.helper.runner.trigger_job.calls()
 
-        assert len(trigger_calls) == 4
+        assert len(runner_calls) == 4
 
-        assert trigger_calls[0][1] == (self.ref_tag, koji_tag_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[0])
-        assert trigger_calls[1][1] == (self.ref_tag, koji_tag_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[1])
-        assert trigger_calls[2][1] == (self.ref_tag, koji_tag_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_validarches[0])
-        assert trigger_calls[3][1] == (self.ref_tag, koji_tag_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_validarches[1])
+        assert runner_calls[0][1] == (
+            self.ref_tag, koji_tag_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[0])
+        assert runner_calls[1][1] == (
+            self.ref_tag, koji_tag_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[1])
+        assert runner_calls[2][1] == (
+            self.ref_tag, koji_tag_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_validarches[0])
+        assert runner_calls[3][1] == (
+            self.ref_tag, koji_tag_msg.ITEM_TYPE, self.ref_tasks[1], self.ref_validarches[1])
 
         assert self.helper.queued_tags == set()
+        assert self.helper.queued_data == dict()
 
     def test_consume_not_primary_instance(self):
         ref_instance = 'ppc'
-        self._create_msg(ref_instance, self.ref_name, self.ref_release, self.ref_tag, self.ref_version)
+        self._create_msg(
+            ref_instance, self.ref_name, self.ref_release, self.ref_tag, self.ref_version)
 
         self.helper.consume(self.ref_message)
 
-        trigger_calls = self.helper.trigger.trigger_job.calls()
+        runner_calls = self.helper.runner.trigger_job.calls()
 
-        assert trigger_calls == []
+        assert runner_calls == []
 
     def test_consume_kojitag_not_pending(self):
         ref_tag = 'f18-updates-testing'
-        self._create_msg(self.ref_instance, self.ref_name, self.ref_release, ref_tag, self.ref_version)
+        self._create_msg(
+            self.ref_instance, self.ref_name, self.ref_release, ref_tag, self.ref_version)
         self.helper.consume(self.ref_message)
 
         assert self.helper.queued_tags == set()
 
     def test_delayed_consume_upgradepath(self):
         ref_tag = 'f18-updates-testing-pending'
+        ref_data = deepcopy(self.ref_data)
+        ref_data['item'] = ref_data['tag'] = ref_tag
+
         self.helper.queued_tags = [ref_tag]
+        self.helper.queued_data = {ref_tag: ref_data}
 
         self.helper.delayed_consume()
 
-        trigger_calls = self.helper.trigger.trigger_job.calls()
+        runner_calls = self.helper.runner.trigger_job.calls()
 
-        assert len(trigger_calls) == 2
+        assert len(runner_calls) == 2
 
-        assert trigger_calls[0][1] == (ref_tag, koji_tag_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[0])
-        assert trigger_calls[1][1] == (ref_tag, koji_tag_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[1])
+        assert runner_calls[0][1] == (
+            ref_tag, koji_tag_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[0])
+        assert runner_calls[1][1] == (
+            ref_tag, koji_tag_msg.ITEM_TYPE, self.ref_tasks[0], self.ref_validarches[1])
 
         assert self.helper.queued_tags == set()
 
     def test_consume_blacklisted_release(self):
         ref_release = '3.el7.1'
         ref_tag = 'epel7-testing-pending'
-        self._create_msg(self.ref_instance, self.ref_name, ref_release, ref_tag, self.ref_version)
+        self._create_msg(
+            self.ref_instance, self.ref_name, ref_release, ref_tag, self.ref_version)
 
         self.helper.consume(self.ref_message)
 
         assert self.helper.queued_tags == set([])
 
     def test_consume_allowed_release(self):
         ref_release = '3.fc20.1'
         ref_tag = 'f20-testing-pending'
-        self._create_msg(self.ref_instance, self.ref_name, ref_release, ref_tag, self.ref_version)
+        self._create_msg(
+            self.ref_instance, self.ref_name, ref_release, ref_tag, self.ref_version)
 
         self.helper.consume(self.ref_message)
 
diff --git a/testing/test_triggers.py b/testing/test_triggers.py
--- a/testing/test_triggers.py
+++ b/testing/test_triggers.py
@@ -1,17 +1,18 @@
 import pytest
 
-from jobtriggers import triggers
+from jobtriggers import runners
 
 
-class TestTriggers():
+class TestRunners():
+
     def setup_method(self, method):
-        self.ref_triggername = 'StreamTrigger'
+        self.ref_runnername = 'StreamRunner'
 
-    def test_get_valid_trigger(self):
-        res = triggers.get_trigger(self.ref_triggername)
+    def test_get_valid_runner(self):
+        res = runners.get_runner(self.ref_runnername)
 
-        assert isinstance(res, triggers.StreamTrigger)
+        assert isinstance(res, runners.StreamRunner)
 
-    def test_get_invalid_trigger(self):
+    def test_get_invalid_runner(self):
         with pytest.raises(KeyError):
-            triggers.get_trigger('NotExistingRandomTrigger')
+            runners.get_runner('NotExistingRandomRunner')
diff --git a/testing/test_utils.py b/testing/test_utils.py
--- a/testing/test_utils.py
+++ b/testing/test_utils.py
@@ -11,6 +11,7 @@
 
 @pytest.mark.usefixtures('prepare')
 class TestUtils():
+
     @pytest.fixture
     def prepare(self, monkeypatch):
         pass
@@ -31,7 +32,7 @@
             with pytest.raises(exc.TriggerError):
                 utils.parse_yaml_from_file('mocked_filename')
 
-    def test_check_should_schedule_true(self):
+    def test_check__should_schedule_true(self):
         file_data = '''
             scheduling:
                 run: True
@@ -43,9 +44,9 @@
         mocked_open = mock.mock_open(read_data=file_data)
 
         with mock.patch('__builtin__.open', mocked_open):
-            assert utils.should_schedule('mocked_filename', 'koji_build')
+            assert utils._should_schedule('mocked_filename', 'koji_build')
 
-    def test_check_should_schedule_false(self):
+    def test_check__should_schedule_false(self):
         file_data = '''
             scheduling:
                 run: False
@@ -57,62 +58,63 @@
         mocked_open = mock.mock_open(read_data=file_data)
 
         with mock.patch('__builtin__.open', mocked_open):
-            assert not utils.should_schedule('mocked_filename', 'koji_build')
+            assert not utils._should_schedule('mocked_filename', 'koji_build')
 
-    def test_check_should_schedule_default(self):
+    def test_check__should_schedule_default(self):
         file_data = '''
             input:
                 args:
                     - koji_build
             '''
         mocked_open = mock.mock_open(read_data=file_data)
 
         with mock.patch('__builtin__.open', mocked_open):
-            assert utils.should_schedule('mocked_filename', 'koji_build')
+            assert utils._should_schedule('mocked_filename', 'koji_build')
 
-    def test_check_should_schedule_false_item_type(self):
+    def test_check__should_schedule_false_item_type(self):
         file_data = '''
             input:
                 args:
                     - koji_tag
             '''
         mocked_open = mock.mock_open(read_data=file_data)
 
         with mock.patch('__builtin__.open', mocked_open):
-            assert not utils.should_schedule('mocked_filename', 'koji_build')
+            assert not utils._should_schedule('mocked_filename', 'koji_build')
 
     def test_distgit_tasks(self, monkeypatch):
         mock_check_output = mock.Mock(return_value='')
-        mock_should_schedule = mock.Mock(return_value=True)
+        mock__should_schedule = mock.Mock(return_value=True)
         monkeypatch.setattr(subprocess, 'check_output', mock_check_output)
-        monkeypatch.setattr(utils, 'should_schedule', mock_should_schedule)
+        monkeypatch.setattr(utils, '_should_schedule', mock__should_schedule)
         monkeypatch.setattr(os, 'listdir', lambda x: ['rpmlint'])
         monkeypatch.setattr(os.path, 'isdir', lambda x: True)
-        monkeypatch.setattr(config, 'distgit_repo_url', 'http://repos.com')
-        monkeypatch.setattr(config, 'distgit_cache_dir', '/path/to')
-        monkeypatch.setattr(config, 'distgit_namespace', 'qa')
-
-        repo = utils.get_repo('firefox')
-        tasks = utils.get_distgit_tasks('firefox', 'koji_build', repo)
-        assert mock_check_output.call_args[0][0][6] == os.path.join('http://repos.com',
-                                                                    'qa',
-                                                                    'firefox.git')
-        assert mock_check_output.call_args[0][0][7] == os.path.join('/path/to', 'firefox')
-        assert mock_should_schedule.call_args[0][0] == os.path.join('/path/to',
-                                                                    'firefox',
-                                                                    'rpmlint',
-                                                                    'runtask.yml')
-        assert mock_should_schedule.call_args[0][1] == 'koji_build'
+        monkeypatch.setattr(config, 'git_cache_dir', '/path/to')
+
+        repo = 'http://pkgs.fedoraproject.org/git/rpms-checks/firefox.git'
+        tasks = utils.discover_tasks('koji_build', repo)
+        import pprint
+        pprint.pprint(mock_check_output.call_args)
+        pprint.pprint(mock__should_schedule.call_args)
+        assert mock_check_output.call_args[0][0][6] == repo
+        assert mock_check_output.call_args[0][0][
+            7] == os.path.join('/path/to', 'firefox')
+        assert mock__should_schedule.call_args[0][0] == os.path.join('/path/to',
+                                                                     'firefox',
+                                                                     'rpmlint',
+                                                                     'runtask.yml')
+        assert mock__should_schedule.call_args[0][1] == 'koji_build'
         assert tasks == ['rpmlint']
 
 
 class TestGetBranch(object):
+
     def test_nvrs(self):
-        assert utils.get_branch('foo-1.2-3.fc20') == 'f20'
-        assert utils.get_branch('foo-1.2-3.fc20.1') == 'f20'
-        assert utils.get_branch('foo-1.2-3.fc22hashgit.fc20') == 'f20'
-        assert utils.get_branch('foo-1.2-3.fc22hashgit.fc20.1') == 'f20'
+        assert utils.get_distgit_branch('foo-1.2-3.fc20') == 'f20'
+        assert utils.get_distgit_branch('foo-1.2-3.fc20.1') == 'f20'
+        assert utils.get_distgit_branch('foo-1.2-3.fc22hashgit.fc20') == 'f20'
+        assert utils.get_distgit_branch('foo-1.2-3.fc22hashgit.fc20.1') == 'f20'
 
     def test_unsupported_dist_tag(self):
         with pytest.raises(exc.TriggerMsgError):
-            utils.get_branch('foo-1.2-3.el7')
+            utils.get_distgit_branch('foo-1.2-3.el7')

