diff --git a/jobtriggers/jobtrigger.py b/jobtriggers/jobtrigger.py
--- a/jobtriggers/jobtrigger.py
+++ b/jobtriggers/jobtrigger.py
@@ -1,3 +1,4 @@
+import os
 import copy
 import yaml
 import itertools
@@ -67,11 +68,21 @@
                 task['tasks'] = item['tasks']
             elif 'discover' in item:
                 discover = item['discover']
+
+                # clone the repo (or fail)
                 repo = discover['repo']
-                branch = discover.get('branch', 'master')
-                task['tasks'] = utils.discover_tasks(task['item_type'], repo, branch)
+                branch = discover.get('branch', 'master').strip() or 'master'
+                fallback = discover.get('fallback_branch', '').strip()
+                repo_dir, branch = utils.clone_repo(repo, branch, fallback)
+
+                # append base_dir, if specified
+                base_dir = discover.get('base_dir', '').strip()
+                repo_dir = os.path.join(repo_dir, base_dir)
+                # fill the task details
+                recursive = discover.get('recursive', False)
                 task['repo'] = repo
                 task['branch'] = branch
+                task['tasks'] = utils.discover_tasks(repo_dir, task['item_type'], recursive)
             else:
                 self.log.error("Rule has invalid `do` section: %r", rule)
 
diff --git a/jobtriggers/utils.py b/jobtriggers/utils.py
--- a/jobtriggers/utils.py
+++ b/jobtriggers/utils.py
@@ -82,35 +82,61 @@
     raise exc.TriggerMsgError('Could not get docker registry url from %d' % build_id)
 
 
-def discover_tasks(item_type, repo, branch='master'):
+def clone_repo(repo, branch='master', fallback=None):
+    """Clones the repo & branch. If the branch can not be cloned, and fallback
+    is set, it tries to clone fallback. If repo can not be cloned, raises exception.
+    Returns full path to the cloned repo, and the cloned branch."""
     dirname = repo.split('/')[-1]
     if dirname.endswith('.git'):
         dirname = dirname[:-len(".git")]
 
-    tasks_dir = os.path.join(config.git_cache_dir, dirname)
+    repo_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):
-        shutil.rmtree(tasks_dir)
+    if os.path.exists(repo_dir):
+        shutil.rmtree(repo_dir)
 
-    cmd = ['git', 'clone', '--branch', branch, '--depth', '1', repo, tasks_dir]
+    cmd = ['git', 'clone', '--branch', branch, '--depth', '1', repo, repo_dir]
 
     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 discover_tasks(item_type, repo, branch='master')
+        if fallback and branch != fallback:
+            if 'Could not find remote branch' in e.output:
+                return clone_repo(repo, branch=fallback)
         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):
-            tasks.append(task)
+    return repo_dir, branch
+
+
+def _discover_formulae(directory, recursive=False):
+    """Returns a list of relative paths to 'runtask.yml' files
+    in the directory.
+    Search is recursive, as long as recursive is set to True.
+    """
+    formulae = []
+    # remove trailing slashes
+    directory = directory.strip().rstrip('/')
+
+    for dirpath, dirs, files in os.walk(directory):
+        if 'runtask.yml' in files:
+            formula = os.path.join(dirpath, 'runtask.yml')
+            # remove the base directory path
+            formulae.append(formula[len(directory)+1:])
+        if not recursive:
+            break
+        # remove hidden dirs
+        # dirs needs to be updated in-place
+        dirs[:] = [d for d in dirs if not d.startswith('.')]
+
+    return formulae
+
+
+def discover_tasks(directory, item_type, recursive=False):
+    """Returns a list of relative paths to 'runtask.yml' files in the directory.
+    Only formulae that are relevant for the item_type are returned.
+    """
+    formulae = _discover_formulae(directory, recursive)
+    tasks = [f for f in formulae if _should_schedule(os.path.join(directory, f), item_type)]
 
     return tasks
diff --git a/testing/test_dist_git_trigger.py b/testing/test_dist_git_trigger.py
--- a/testing/test_dist_git_trigger.py
+++ b/testing/test_dist_git_trigger.py
@@ -6,6 +6,8 @@
 from jobtriggers import dist_git_commit_msg
 from jobtriggers import utils
 
+def mock_clone_repo(repo, branch='master', fallback=None):
+    return '/tmp/path_to_repo', branch
 
 @pytest.mark.usefixtures('prepare')
 class TestDistGitCommitJobTrigger():
@@ -37,6 +39,7 @@
 
         mock_discover_tasks = mock.Mock(return_value=[])
         monkeypatch.setattr(utils, 'discover_tasks', mock_discover_tasks)
+        monkeypatch.setattr(utils, 'clone_repo', mock_clone_repo)
 
         mock_critpath = {"pkgs": {"f18": [], "master": []}}
         mock_get_critpath = mock.Mock(return_value=mock_critpath)
diff --git a/testing/test_jobtrigger.py b/testing/test_jobtrigger.py
--- a/testing/test_jobtrigger.py
+++ b/testing/test_jobtrigger.py
@@ -12,6 +12,9 @@
 class BogusJobTrigger(jobtrigger.JobTrigger):
     config_key = 'taskotron.bogusjobtrigger.enabled'
 
+def mock_clone_repo(repo, branch='master', fallback=None):
+    return '/tmp/path_to_repo', branch
+
 
 @pytest.mark.usefixtures('prepare')
 class TestJobtrigger():
@@ -26,6 +29,8 @@
         mock_discover_tasks = mock.Mock(return_value=self.ref_discovered_tasks)
         monkeypatch.setattr(utils, 'discover_tasks', mock_discover_tasks)
 
+        monkeypatch.setattr(utils, 'clone_repo', mock_clone_repo)
+
         self.ref_item = "package_foobar-1.0.8-fc18"
         self.ref_item_type = "bar"
         self.ref_name = "package_foobar"
@@ -94,6 +99,21 @@
         assert tasks[0]['repo'] == ref_repo
         assert tasks[0]['branch'] == 'mybranch'
 
+    def test__tasks_from_rule_discover_recursive(self, monkeypatch):
+        utils_dingus = Dingus(discover_tasks__returns=self.ref_discovered_tasks)
+        monkeypatch.setattr(utils, 'discover_tasks', utils_dingus.discover_tasks)
+
+        ref_repo = 'http://bogus.repo/firefox.git'
+        rule = {'do': [{'discover': {'repo': ref_repo, 'recursive': True}}]}
+        tasks = self.helper._tasks_from_rule(rule, self.ref_message_data)
+
+        discover_calls = utils_dingus.calls()
+        assert len(discover_calls) == 1
+        assert discover_calls[0][0] == 'discover_tasks'
+        assert discover_calls[0][1][0] == '/tmp/path_to_repo/'
+        assert discover_calls[0][1][1] == self.ref_message_data['item_type']
+        assert discover_calls[0][1][2] == True
+
     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)
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,6 +10,9 @@
 params = [('someowner', koji_build_msg.ITEM_TYPE_PACKAGE, "KojiBuildPackageCompleted"),
           ('containerbuild', koji_build_msg.ITEM_TYPE_DOCKER, "KojiBuildDockerCompleted")]
 
+def mock_clone_repo(repo, branch='master', fallback=None):
+    return '/tmp/path_to_repo', branch
+
 @pytest.mark.usefixtures('prepare')
 @pytest.mark.parametrize('buildtype', params)
 class TestKojiBuildCompletedJobTrigger():
@@ -57,6 +60,8 @@
         mock_discover_tasks = mock.Mock(return_value=[])
         monkeypatch.setattr(utils, 'discover_tasks', mock_discover_tasks)
 
+        monkeypatch.setattr(utils, 'clone_repo', mock_clone_repo)
+
         mock_critpath = {"pkgs": {"f18": [], "master": []}}
         mock_get_critpath = mock.Mock(return_value=mock_critpath)
         monkeypatch.setattr(utils, 'parse_yaml_from_file', mock_get_critpath)
diff --git a/testing/test_utils.py b/testing/test_utils.py
--- a/testing/test_utils.py
+++ b/testing/test_utils.py
@@ -82,30 +82,80 @@
         with mock.patch('__builtin__.open', mocked_open):
             assert not utils._should_schedule('mocked_filename', 'koji_build')
 
-    def test_distgit_tasks(self, monkeypatch):
+    def test_clone_repo(self, monkeypatch):
+        mock_path_exists = mock.Mock(return_value=False)
         mock_check_output = mock.Mock(return_value='')
-        mock__should_schedule = mock.Mock(return_value=True)
+
+        monkeypatch.setattr(os.path, 'exists', mock_path_exists)
+        monkeypatch.setattr(config, 'git_cache_dir', '/path/to')
         monkeypatch.setattr(subprocess, 'check_output', mock_check_output)
-        monkeypatch.setattr(utils, '_should_schedule', mock__should_schedule)
-        monkeypatch.setattr(os, 'listdir', lambda x: ['rpmlint'])
-        monkeypatch.setattr(os.path, 'isdir', lambda x: True)
+
+        repo_dir, branch = utils.clone_repo('http://bogus.url/repo')
+        assert repo_dir == '/path/to/repo'
+        assert branch == 'master'
+
+        repo_dir, branch = utils.clone_repo('http://bogus.url/repo.git', branch='develop')
+        assert repo_dir == '/path/to/repo'
+        assert branch == 'develop'
+
+        repo_dir, branch = utils.clone_repo('http://bogus.url/repo', fallback='fallback')
+        assert repo_dir == '/path/to/repo'
+        assert branch == 'master'
+
+    def test_clone_repo_missing_brach(self, monkeypatch):
+        mock_path_exists = mock.Mock(return_value=False)
+        def mock_check_output(params, *args, **kwargs):
+            if 'ok_branch' not in params:
+                exc = subprocess.CalledProcessError(None, None, 'Could not find remote branch')
+                raise exc
+            return
+
+        monkeypatch.setattr(os.path, 'exists', mock_path_exists)
         monkeypatch.setattr(config, 'git_cache_dir', '/path/to')
+        monkeypatch.setattr(subprocess, 'check_output', mock_check_output)
 
-        repo = 'http://pkgs.fedoraproject.org/git/test-rpms/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']
+        # sanity check to be sure the mock_check_output works
+        repo_dir, branch = utils.clone_repo('/repo', branch='ok_branch')
+        assert repo_dir == '/path/to/repo'
+        assert branch == 'ok_branch'
+        with pytest.raises(exc.TriggerError):
+            utils.clone_repo('', branch='missing_branch')
+
+        # actual check
+        repo_dir, branch = utils.clone_repo('/repo', branch='master', fallback='ok_branch')
+        assert repo_dir == '/path/to/repo'
+        assert branch == 'ok_branch'
+
+        with pytest.raises(exc.TriggerError):
+            utils.clone_repo('', branch='missing_branch', fallback='master')
+
+    def test__discover_formulae(self, monkeypatch):
+        def mock_os_walk(directory):
+            yield (directory, ['task_1', 'nontask'], ['runtask.yml'])
+            yield ('%s/nontask' % directory, [], [])
+            yield ('%s/task_1' % directory, [], ['runtask.yml'])
+
+        monkeypatch.setattr(os, 'walk', mock_os_walk)
+
+        formulae = utils._discover_formulae('  /path/to/   ', recursive=True)
+        assert len(formulae) == 2
+        assert formulae == ['runtask.yml', 'task_1/runtask.yml']
+
+        formulae = utils._discover_formulae('/path/to')
+        assert len(formulae) == 1
+        assert formulae == ['runtask.yml']
+
+    def test_discover_tasks(self, monkeypatch):
+        mock__discover_formulae = mock.Mock(return_value=['task1.yml', 'task2.yml'])
+        def mock__should_schedule(task, item_type):
+            return task == '/path/to/git/task2.yml' and item_type == 'fake_type'
+
+        monkeypatch.setattr(utils, '_discover_formulae', mock__discover_formulae)
+        monkeypatch.setattr(utils, '_should_schedule', mock__should_schedule)
 
+        tasks = utils.discover_tasks('/path/to/git', 'fake_type')
+        assert len(tasks) == 1
+        assert tasks == ['task2.yml']
 
 class TestGetBranch(object):
 

