diff --git a/docs/source/taskyaml.rst b/docs/source/taskyaml.rst
--- a/docs/source/taskyaml.rst
+++ b/docs/source/taskyaml.rst
@@ -49,7 +49,7 @@
 .. code-block:: yaml
 
   input:
-      args: arch, envr
+      args: arch,envr
 
 Valid args include:
 
@@ -96,40 +96,29 @@
       - name: using directivename for something
         directivename:
             arg1: value1
-            arg2: "{{ some_variable }}"
+            arg2: ${some_variable}
         export: somestep_output
 
 
 Variable Storage and Syntax
 ---------------------------
 
-The task yaml file uses standard `jinja2 <http://jinja.pocoo.org/>`_ syntax for
-variables. Variables can be created during task execution or provided by default
-by the task runner.
+The task yaml file uses standard `string.Template`_ syntax for variables. You
+can use ``$variable`` or ``${variable}`` format, and you need to make the dollar
+sign doubled if you want to include it literally (``$$not_a_variable``).
+Variables can be created during task execution or provided by default by the
+task runner.
 
-.. note::
-
-  An unfortunate side-effect of using jinja2 syntax with yaml files is that the
-  yaml parser tends to view variables as invalid dictionaries. If the variable
-  is directly following a colon, make sure to surround it in quotes so that the
-  yaml parser interprets the variable as a string.
-
-  .. code-block:: yaml
-
-    # this will not parse well
-    arg1: {{ some_variable }}
-
-    # this will parse properly
-    arg1: "{{ some_variable }}"
+.. _string.Template: https://docs.python.org/2.7/library/string.html?highlight=string#template-strings
 
 Provided Variables
 ------------------
 
 The task runner provides the following variables without a need for them to be
 explicitly specified:
 
-  * workdir
-  * jobid
+  * ``workdir``
+  * ``jobid``
 
 .. note::
 
@@ -155,7 +144,7 @@
 
       - name: second step
         directive_two:
-            arg1: "{{ firststep_output }}"
+            arg1: ${firststep_output}
 
 In this example, the output from ``first step`` is stored as ``firststep_output``
 and later used by ``second step``. Once the ``first step`` is executed, its
@@ -190,7 +179,7 @@
 bodhi
 -----
 
-This directive is used to download specific packages from 
+This directive is used to download specific packages from
 `bodhi <https://admin.fedoraproject.org/updates/>`_.
 
 createrepo
@@ -201,8 +190,8 @@
 dummy
 -----
 
-Primarily a testing directive, it simply returns a status and 
-optionally a message. Also works as a useful placeholder while 
+Primarily a testing directive, it simply returns a status and
+optionally a message. Also works as a useful placeholder while
 writing new tasks.
 
 koji
diff --git a/docs/source/writingtasks.rst b/docs/source/writingtasks.rst
--- a/docs/source/writingtasks.rst
+++ b/docs/source/writingtasks.rst
@@ -100,7 +100,7 @@
   maintainer: somefasuser
 
   input:
-      args: arch, bodhi_id
+      args: arch,bodhi_id
 
   environment:
       rpm:
@@ -125,7 +125,7 @@
   # this is a trivial example that doesn't do everything we want it to do, more
   # details will be added later in the example
   def run_mytask(rpmfiles, bodhi_id):
-      print "Running mytask on %s" % (str(bodhi_id))
+      print "Running mytask on %s" % bodhi_id
 
 
 .. code-block:: yaml
@@ -137,8 +137,8 @@
       - name: download bodhi update
         bodhi:
             action: download
-            bodhi_id: "{{ bodhi_id }}"
-            arch: "{{ arch }}"
+            bodhi_id: ${bodhi_id}
+            arch: ${arch}
         export: bodhi_downloads
 
 
@@ -149,19 +149,19 @@
         python:
             file: mytask.py
             callable: run_mytask
-            rpmfiles: "{{ bodhi_downloads }}"
-            bodhi_id: "{{ bodhi_id }}"
+            rpmfiles: ${bodhi_downloads}
+            bodhi_id: ${bodhi_id}
         export: mytask_output
 
 
       # this assumes that the output from mytask is valid TAP
       - name: report mytask results to resultsdb
         resultsdb:
-            results: "{{ mytask_output }}"
+            results: ${mytask_output}
             checkname: 'mytask'
 
 .. this needs to be fixed, this output is doctored since it doesn't actually
-   work. 
+   work.
 
 We can execute this new task using::
 
@@ -209,7 +209,7 @@
   # details will be added later in the example
   def run_mytask(rpmfiles):
       for rpmfile in rpmfiles:
-          print "Running mytask on %s" % (str(rpmfile))
+          print "Running mytask on %s" % rpmfile
 
 To create valid TAP, we want to populate a :py:class:`libtaskotron.check.CheckDetail`
 object and use :py:meth:`libtaskotron.check.export_TAP` to generate valid TAP13
@@ -222,11 +222,11 @@
   def run_mytask(rpmfiles, bodhi_id):
       """run through all passed in rpmfiles and emit TAP13 saying they all passed"""
 
-      print "Running mytask on %s" % (str(bodhi_id))
+      print "Running mytask on %s" % bodhi_id
 
       details = []
       result = 'PASSED'
-      summary = 'mycheck %s for %s' % (result, str(bodhi_id))
+      summary = 'mycheck %s for %s' % (result, bodhi_id)
       detail = check.CheckDetail(rpmfile, check.ReportType.BODHI_UPDATE,
                                   result, summary)
       for rpmfile in rpmfiles:
@@ -253,7 +253,7 @@
 
 .. note::
   Reporting is disabled in the default configuration for Taskotron. While it is
-  possible to set up a local resultsdb instance to check reporting, we want 
+  possible to set up a local resultsdb instance to check reporting, we want
   to make it easier than that and will update these docs when that easier method
   is available.
 
diff --git a/libtaskotron.spec b/libtaskotron.spec
--- a/libtaskotron.spec
+++ b/libtaskotron.spec
@@ -19,7 +19,6 @@
 Requires:       pyOpenSSL
 Requires:       python-pycurl
 Requires:       python-urlgrabber
-Requires:       python-jinja2
 Requires:       pytap13
 Requires:       resultsdb_api
 Requires:       python-bayeux
@@ -35,7 +34,6 @@
 BuildRequires:  python-bunch
 BuildRequires:  python-dingus
 BuildRequires:  python-urlgrabber
-BuildRequires:  python-jinja2
 BuildRequires:  koji
 BuildRequires:  pytap13
 BuildRequires:  python-bayeux
diff --git a/libtaskotron/runner.py b/libtaskotron/runner.py
--- a/libtaskotron/runner.py
+++ b/libtaskotron/runner.py
@@ -3,7 +3,6 @@
 import os.path
 import argparse
 import imp
-from jinja2 import Environment
 import copy
 
 from libtaskotron import taskyaml
@@ -19,7 +18,6 @@
         self.envdata = argdata
         self.working_data = {}
         self.directives = {}
-        self.jinja_env = None
         self.workdir = workdir or tempfile.mkdtemp(prefix="task-",
                                       dir=config.get_config().tmpdir)
 
@@ -46,28 +44,22 @@
         loaded_directive = imp.load_source(real_name, directive_file)
         self.directives[directive_name] = loaded_directive
 
-    def _get_jinja_env(self):
-        if not self.jinja_env:
-            self.jinja_env = Environment()
-            self.jinja_env.globals = self.envdata
-        return self.jinja_env
-
     def _render_action(self, action):
+        '''Take an action and replace all included variables with actual values
+        from :attr:`env_data` and :attr:`working_data`. See :meth:`do_actions`
+        to see how an action looks like.
+
+        :param dict action: An action specification parsed from the task yaml
+                            file
+        :return: a rendered action
+        :rtype: dict
+        '''
         # copy the input so that we don't disrupt what we're processing
         rendered_action = copy.deepcopy(action)
 
-        # any action is going to be a dict with 0 or more embedded dicts
-        for action_key in action:
-            jinja_env = self._get_jinja_env()
-
-            if isinstance(action[action_key], dict):
-                action_line = action[action_key]
-                for keyval in action_line:
-                    input_action = action[action_key][keyval]
-                    arg_template = jinja_env.from_string(input_action)
-
-                    rendered_line = arg_template.render(self.working_data)
-                    rendered_action[action_key][keyval] = rendered_line
+        variables = copy.deepcopy(self.envdata)
+        variables.update(self.working_data)
+        taskyaml.replace_vars_in_action(rendered_action, variables)
 
         return rendered_action
 
@@ -79,6 +71,12 @@
                                  str(action))
 
     def do_single_action(self, action):
+        '''Execute a single action from the task. See :meth:`do_actions` to see
+        how an action looks like.
+
+        :param dict action: An action specification parsed from the task yaml
+                            file
+        '''
         directive_name = self._extract_directive_from_action(action)
 
         rendered_action = self._render_action(action)
@@ -97,6 +95,15 @@
             self.working_data[action['export']] = output
 
     def do_actions(self):
+        '''Sequentially run all actions for a task. An 'action' is a single step
+        under the ``task:`` key. An example action looks like::
+
+          - name: download rpms from koji
+            koji:
+              action: download
+              envr: $envr
+              arch: $arch
+        '''
         if 'task' not in self.taskdata or not self.taskdata['task']:
             raise TaskotronYamlError("At least one task should be specified"
                                      " in input yaml file")
diff --git a/libtaskotron/taskyaml.py b/libtaskotron/taskyaml.py
--- a/libtaskotron/taskyaml.py
+++ b/libtaskotron/taskyaml.py
@@ -1,4 +1,15 @@
+# -*- coding: utf-8 -*-
+# Copyright 2014, Red Hat, Inc.
+# License: GNU General Public License version 2 or later
+
+'''Methods for operating with a task YAML file'''
+
+import collections
+import string
+
 import yaml
+from libtaskotron import exceptions as exc
+from libtaskotron.logger import log
 
 
 def parse_yaml_from_file(filename):
@@ -9,3 +20,98 @@
 def parse_yaml(contents):
     return yaml.safe_load(contents)
 
+
+def _replace_vars(text, variables):
+    '''Go through ``text`` and replace all variables (in the form of what is
+    supported by :class:`string.Template`) with their values, as provided in
+    ``variables``. This is used for variable expansion in the task yaml file.
+
+    :param str text: input text where to search for variables
+    :param dict variables: names (keys) and values of variables to replace
+    :return: if ``text`` contains just a single variable and nothing else, the
+             variable value is directly returned (i.e. with matching type, not
+             cast to ``str``). If ``text`` contains something else as well
+             (other variables or text), a string is returned.
+    :raise TaskotronYamlError: if ``text`` contains a variable that is not
+                               present in ``variables``, or if the variable
+                               syntax is incorrect
+    '''
+
+    try:
+        # try to find the first match
+        match = string.Template.pattern.search(text)
+
+        if not match:
+            return text
+
+        # There are 4 groups in the pattern: 1 - escaped, 2 - named, 3 - braced,
+        # 4 - invalid. Group 0 returns the whole match.
+        if match.group(0) == text and (match.group(2) or match.group(3)):
+            # We found a single variable and nothing more. We shouldn't return
+            # a string, but the exact value, so that we don't lose value type.
+            # This makes it possible to pass lists, dicts, etc as variables.
+            var_name = match.group(2) or match.group(3)
+            return variables[var_name]
+
+        # Now it's clear there's also something else in `text` than just a
+        # single variable. We will replace all variables and return a string
+        # again.
+        output = string.Template(text).substitute(variables)
+        return output
+
+    except KeyError as e:
+        raise exc.TaskotronYamlError("The task yaml file includes a variable, "
+            "but no value has been provided for it: %s" % e)
+
+    except ValueError as e:
+        raise exc.TaskotronYamlError("The task yaml file includes an incorrect "
+            "variable definition. Dollar signs must be doubled if they "
+            "shouldn't be considered as a variable denotation.\n"
+            "Error: %s\n"
+            "Text: %s" % (e, text))
+
+
+def replace_vars_in_action(action, variables):
+    '''Find all variables that are leaves (in a tree sense) in an action. Leaves
+    are variables which can no longer be traversed, i.e. "primitive" types like
+    ``str`` or ``int``. Non-leaves are containers like ``dict`` or ``list``.
+
+    For all leaves, call :func:`_replace_vars` and update their value with
+    the function's output.
+
+    :param dict action: An action specification parsed from the task yaml file.
+                        See :meth:`.Runner.do_actions` to see what an action
+                        looks like.
+    :param dict variables: names (keys) and values of variables to replace
+    :raise TaskotronYamlError: if ``text`` contains a variable that is not
+                               present in ``variables``, or if the variable
+                               syntax is incorrect
+    '''
+
+    visited = []  # all visited nodes in a tree
+    stack = [action]  # nodes waiting for inspection
+
+    while stack:
+        vertex = stack.pop()
+
+        if vertex in visited:
+            continue
+
+        visited.append(vertex)
+        children = [] # list of tuples (index/key, child_value)
+
+        if isinstance(vertex, collections.MutableMapping):
+            children = vertex.items()  # list of (key, value)
+        elif isinstance(vertex, collections.MutableSequence):
+            children = list(enumerate(vertex))  # list of (index, value)
+        else:
+            log.warn("Unknown structure '%s' in YAML file, this shouldn't "
+                     "happen: %s", type(vertex), vertex)
+
+        for index, child_val in children:
+            if isinstance(child_val, basestring):
+                # leaf node and a string, replace variables
+                vertex[index] = _replace_vars(child_val, variables)
+            elif isinstance(child_val, collections.Iterable):
+                # traversable further down, mark for visit
+                stack.append(child_val)
diff --git a/requirements.txt b/requirements.txt
--- a/requirements.txt
+++ b/requirements.txt
@@ -4,7 +4,6 @@
 pyaml==13.07.1
 pytest>=2.4.2
 pytest-cov>=1.6
-jinja2>=2.7.2
 bayeux>=0.8
 yamlish>=0.11
 pytap13>=0.0.2
diff --git a/testing/test_runner.py b/testing/test_runner.py
--- a/testing/test_runner.py
+++ b/testing/test_runner.py
@@ -210,7 +210,7 @@
         ref_messagename = 'variable_messsage'
         ref_action = {'name': 'Dummy Action',
                       'dummy': {'result': 'pass',
-                                'msg': '{{ %s }}' % ref_messagename},
+                                'msg': '${%s}' % ref_messagename},
                       'export': '%s' % ref_exportname}
 
         test_runner = runner.Runner(self.ref_taskdata, self.ref_inputdata)
@@ -262,7 +262,7 @@
         ref_messagename = 'variable_messsage'
         ref_action = {'name': 'Dummy Action',
                       'dummy': {'result': 'pass',
-                                'msg': '{{ %s }}' % ref_messagename},
+                                'msg': '${%s}' % ref_messagename},
                       'export': '%s' % ref_exportname}
 
         self.ref_taskdata = {'task': [ref_action, ref_action]}
@@ -301,15 +301,47 @@
         ref_message = 'This is a variable message'
         ref_messagename = 'variable_messsage'
         ref_action = {'name': 'test dummy action', 'dummy':
-            {'result': 'pass', 'msg': '{{ %s }}' % ref_messagename}}
+            {'result': 'pass', 'msg': '${%s}' % ref_messagename}}
 
         test_runner = runner.Runner(self.ref_taskdata, self.ref_inputdata)
         test_runner.working_data[ref_messagename] = ref_message
 
         test_rendered_action = test_runner._render_action(ref_action)
 
         assert test_rendered_action['dummy']['msg'] == ref_message
 
+    def test_runner_render_list_variable(self):
+        ref_var = ['foo', 'bar']
+        ref_varname = 'testvar'
+        ref_action = {'name': 'test dummy action', 'dummy':
+            {'result': 'pass', 'msg': '${%s}' % ref_varname}}
+
+        test_runner = runner.Runner(self.ref_taskdata, self.ref_inputdata)
+        test_runner.working_data[ref_varname] = ref_var
+
+        test_rendered_action = test_runner._render_action(ref_action)
+
+        assert test_rendered_action['dummy']['msg'] == ref_var
+
+    def test_runner_render_multi_variable(self):
+        ref_var1 = 'foo'
+        ref_var1name = 'testvar1'
+        ref_var2 = 'bar'
+        ref_var2name = 'testvar2'
+        ref_action = {'name': 'test dummy action', 'dummy':
+            {'result': 'pass', 'msg': '--foo ${%s} --bar ${%s}' %
+            (ref_var1name, ref_var2name)}}
+
+        test_runner = runner.Runner(self.ref_taskdata, self.ref_inputdata)
+        test_runner.working_data[ref_var1name] = ref_var1
+        test_runner.working_data[ref_var2name] = ref_var2
+
+        test_rendered_action = test_runner._render_action(ref_action)
+
+        assert (test_rendered_action['dummy']['msg'] == '--foo %s --bar %s' %
+                (ref_var1, ref_var2))
+
+
 class TestRunnerProcessArgs():
 
     def test_process_args_koji_build(self):
diff --git a/testing/test_taskyaml.py b/testing/test_taskyaml.py
--- a/testing/test_taskyaml.py
+++ b/testing/test_taskyaml.py
@@ -1,23 +1,32 @@
-import yaml
+# -*- coding: utf-8 -*-
+# Copyright 2014, Red Hat, Inc.
+# License: GNU General Public License version 2 or later
+
+'''Unit tests for libtaskotron/taskyaml.py'''
+
+import copy
 
+import yaml
+import pytest
 from libtaskotron import taskyaml
+import libtaskotron.exceptions as exc
+
 
-ref_filename = '/home/testuser/testtask.yml'
 ref_taskname = 'testtask'
 ref_taskdesc = 'this is a test task'
 ref_maintainer = 'testuser'
 ref_deps = ['libtaskotron', 'libpanacea']
 ref_input = {'args': 'arch, envr'}
 ref_actions = []
 
 
-def create_yamldata(name=ref_taskname, desc=ref_taskdesc, maint= ref_maintainer,
-                    deps=ref_deps, input=ref_input, actions=ref_actions):
+def create_yamldata(name=ref_taskname, desc=ref_taskdesc, maint=ref_maintainer,
+                    deps=ref_deps, input_=ref_input, actions=ref_actions):
     return {'name': name,
             'desc': desc,
             'maintainer': maint,
             'deps': deps,
-            'input': input,
+            'input': input_,
             'task': actions}
 
 
@@ -58,3 +67,122 @@
         test_task = taskyaml.parse_yaml(yaml.safe_dump(taskdata))
 
         assert len(test_task['task']) == 3
+
+
+class TestReplaceVarsInAction(object):
+
+    def setup_method(self, method):
+        self.action = {
+            'name': 'run foo${foo}',
+            'export': '${export}',
+            'python': {
+                'file': 'somefile.py',
+                'args': ['arg1', '${number}', 'arg2', '${list}', '${dict}'],
+                'cmdline': '-s ${foo} -i ${number}',
+                'nest': {
+                    'deepnest': '${dict}',
+                    'null': '${null}',
+                }
+            }
+        }
+        self.vars = {
+            'foo': 'bar',
+            'export': 'a:b\nc:d\n-e',
+            'number': 1,
+            'list': [1, 'item'],
+            'dict': {'key': 'val', 'pi': 3.14},
+            'null': None,
+            'extra': 'juicy',
+        }
+
+    def test_complex(self):
+        rend_action = copy.deepcopy(self.action)
+        rend_action['name'] = 'run foo%s' % self.vars['foo']
+        rend_action['export'] = self.vars['export']
+        rend_action['python']['args'] = ['arg1', self.vars['number'], 'arg2',
+                                         self.vars['list'], self.vars['dict']]
+        rend_action['python']['cmdline'] = '-s %s -i %s' % (self.vars['foo'],
+                                                            self.vars['number'])
+        rend_action['python']['nest']['deepnest'] = self.vars['dict']
+        rend_action['python']['nest']['null'] = self.vars['null']
+
+        taskyaml.replace_vars_in_action(self.action, self.vars)
+        assert self.action == rend_action
+
+    def test_raise(self):
+        with pytest.raises(exc.TaskotronYamlError):
+            taskyaml.replace_vars_in_action(self.action, {})
+
+        del self.vars['number']
+        with pytest.raises(exc.TaskotronYamlError):
+            taskyaml.replace_vars_in_action(self.action, self.vars)
+
+
+class TestReplaceVars(object):
+
+    def test_standadlone_var_str(self):
+        res = taskyaml._replace_vars('${foo}', {'foo': 'hedgehog'})
+        assert res == 'hedgehog'
+
+    def test_standalone_var_nonstr(self):
+        res = taskyaml._replace_vars('${foo}', {'foo': ['chicken', 'egg']})
+        assert res == ['chicken', 'egg']
+
+    def test_standadlone_var_str_alternative(self):
+        res = taskyaml._replace_vars('$foo', {'foo': 'hedgehog'})
+        assert res == 'hedgehog'
+
+    def test_standalone_var_nonstr_alternative(self):
+        res = taskyaml._replace_vars('$foo', {'foo': ['chicken', 'egg']})
+        assert res == ['chicken', 'egg']
+
+    def test_multivar(self):
+        res = taskyaml._replace_vars('a ${b} c${d}', {'b': 'bb', 'd': 'dd'})
+        assert res == 'a bb cdd'
+
+    def test_empty_space(self):
+        '''Some empty space around a single variable is like multi-var, i.e.
+        a string is always returned'''
+        res = taskyaml._replace_vars(' ${foo}', {'foo': 'bar'})
+        assert res == ' bar'
+
+        res = taskyaml._replace_vars(' ${foo}', {'foo': 42})
+        assert res == ' 42'
+
+    def test_recursive_content(self):
+        '''The content of a variable contains more variable names. But those
+        must not be expanded.'''
+        text = '${foo} ${bar}'
+        varz = {'foo': '${foo} ${bar}',
+                'bar': '${foo} ${bar} ${baz}'
+               }
+        res = taskyaml._replace_vars(text, varz)
+        assert res == '%s %s' % (varz['foo'], varz['bar'])
+
+    def test_empty(self):
+        res = taskyaml._replace_vars('', {'extra': 'var'})
+        assert res == ''
+
+    def test_same_var(self):
+        '''Only variables must get replaced, not same-sounding text'''
+        res = taskyaml._replace_vars('foo ${foo}', {'foo': 'bar'})
+        assert res == 'foo bar'
+
+    def test_combined_syntax(self):
+        '''Both $foo and ${foo} syntax must work, together with escaping'''
+        res = taskyaml._replace_vars('foo $foo ${foo} $$foo', {'foo': 'bar'})
+        assert res == 'foo bar bar $foo'
+
+    def test_raise_missing_var(self):
+        with pytest.raises(exc.TaskotronYamlError):
+            taskyaml._replace_vars('${foo}', {'bar': 'foo'})
+
+    def test_raise_missing_vars(self):
+        with pytest.raises(exc.TaskotronYamlError):
+            taskyaml._replace_vars('${foo} ${bar}', {'bar': 'foo'})
+
+    def test_raise_bad_escape(self):
+        '''Dollars must be doubled'''
+        with pytest.raises(exc.TaskotronYamlError):
+            taskyaml._replace_vars('${foo} $', {'foo': 'bar'})
+

