diff --git a/carnival/__init__.py b/carnival/__init__.py
index 7b1beb8..a46637f 100644
--- a/carnival/__init__.py
+++ b/carnival/__init__.py
@@ -1,8 +1,8 @@
 import sys
 import dotenv
 import os
-from carnival.step import Step, InlineStep
 from carnival.hosts.base import Host, Connection, Result
+from carnival.steps import Step, InlineStep
 from carnival.role import Role, SingleRole
 from carnival.hosts.local import LocalHost, localhost_connection
 from carnival.hosts.ssh import SshHost
diff --git a/carnival/exceptions.py b/carnival/exceptions.py
index 024c5c1..d520029 100644
--- a/carnival/exceptions.py
+++ b/carnival/exceptions.py
@@ -10,9 +10,3 @@ class GlobalConnectionError(CarnivalException):
     """
     Global connection switching error
     """
-
-
-class StepValidationError(CarnivalException):
-    """
-    Ошибка валидации шага
-    """
diff --git a/carnival/steps/__init__.py b/carnival/steps/__init__.py
new file mode 100644
index 0000000..6e51a27
--- /dev/null
+++ b/carnival/steps/__init__.py
@@ -0,0 +1,7 @@
+from carnival.steps.step import Step, InlineStep
+
+
+__all__ = (
+    "Step",
+    "InlineStep",
+)
diff --git a/carnival/steps/_validator_cache.py b/carnival/steps/_validator_cache.py
new file mode 100644
index 0000000..da3be50
--- /dev/null
+++ b/carnival/steps/_validator_cache.py
@@ -0,0 +1,46 @@
+"""
+    Кеширование валидаторов для Steps
+    Кеширует значение по Type[Step], Host, fact_id:str(задается в валидаторе)
+"""
+
+import typing
+
+if typing.TYPE_CHECKING:
+    from carnival import Host
+    from carnival.steps.validators import StepValidatorBase
+
+
+StepValidatorBaseT = typing.TypeVar("StepValidatorBaseT", bound="StepValidatorBase")
+__vc: typing.Dict[str, typing.Optional[str]] = {}
+
+
+def __build_vc_key(step_class: typing.Type[StepValidatorBaseT], host: "Host", fact_id: str) -> str:
+    from carnival.utils import get_class_full_name
+    return f"{get_class_full_name(step_class)}::{host.addr}::{fact_id}"
+
+
+def try_get(
+    step_class: typing.Type[StepValidatorBaseT],
+    host: "Host",
+    fact_id: str,
+) -> typing.Tuple[bool, typing.Optional[str]]:
+    global __vc
+
+    return (
+        __build_vc_key(step_class=step_class, host=host, fact_id=fact_id) in __vc,
+        __vc.get(
+            __build_vc_key(step_class=step_class, host=host, fact_id=fact_id),
+            None,
+        )
+    )
+
+
+def set(step_class: typing.Type[StepValidatorBaseT], host: "Host", fact_id: str, val: typing.Optional[str]) -> None:
+    global __vc
+    cachekey = __build_vc_key(step_class=step_class, host=host, fact_id=fact_id)
+
+    is_exist, *_ = try_get(step_class=step_class, host=host, fact_id=fact_id)
+    if is_exist:
+        raise ValueError(f"Broken cache: '{cachekey}' already exist!")
+
+    __vc[cachekey] = val
diff --git a/carnival/step.py b/carnival/steps/step.py
similarity index 80%
rename from carnival/step.py
rename to carnival/steps/step.py
index ea2dbf5..7499c74 100644
--- a/carnival/step.py
+++ b/carnival/steps/step.py
@@ -1,8 +1,8 @@
 import abc
 import typing
 
-
 if typing.TYPE_CHECKING:
+    from carnival.steps.validators import StepValidatorBase
     from carnival import Connection
 
 
@@ -25,27 +25,34 @@ class Step:
     >>>         ...
 
     """
+
     def __init__(self) -> None:
         pass
 
     def get_name(self) -> str:
         return self.__class__.__name__
 
-    def validate(self, c: "Connection") -> None:
+    def get_validators(self) -> typing.List["StepValidatorBase"]:
+        """
+        Получить список валидаторов для метода `.validate`
+        """
+        return []
+
+    def validate(self, c: "Connection") -> typing.List[str]:
         """
         Валидатор шага, запускается перед выполнением
-        Должен выкидывать .StepValidationError в случае ошибки
+        :param c: Конект к хосту
+        :return: Список ошибок
+        """
 
-        :param host: На котором будет выполнен шаг
+        errors: typing.List[str] = []
 
-        :raises StepValidationError: в случае ошибок валидации
+        for validator in self.get_validators():
+            err = validator.validate(c=c)
+            if err is not None:
+                errors.append(err)
 
-        >>> from carnival.exceptions import StepValidationError
-        >>> ...
-        >>> def validate(self, c: "Connection") -> None:
-        >>>     raise StepValidationError("Step validation is not implemented")
-        """
-        pass
+        return errors
 
     @abc.abstractmethod
     def run(self, c: "Connection") -> typing.Any:
diff --git a/carnival/steps/validators.py b/carnival/steps/validators.py
new file mode 100644
index 0000000..4b673a2
--- /dev/null
+++ b/carnival/steps/validators.py
@@ -0,0 +1,81 @@
+import typing
+import abc
+
+from carnival import cmd
+from carnival.steps import _validator_cache
+from carnival.templates import render
+
+from jinja2.exceptions import UndefinedError, TemplateNotFound, TemplateSyntaxError
+
+
+if typing.TYPE_CHECKING:
+    from carnival import Connection
+    from carnival.steps.step import Step
+
+
+class StepValidatorBase:
+    def __init__(self, step: "Step"):
+        self.step = step
+
+    @abc.abstractmethod
+    def validate(self, c: "Connection") -> typing.Optional[str]:
+        raise NotImplementedError
+
+
+class InlineValidator(StepValidatorBase):
+    def __init__(
+        self,
+        if_err_true_fn: typing.Callable[["Connection"], bool],
+        error_message: str,
+        fact_id_for_caching: typing.Optional[str] = None,
+    ):
+        self.if_err_true_fn = if_err_true_fn
+        self.error_message = error_message
+        self.fact_id_for_caching = fact_id_for_caching
+
+    def validate(self, c: "Connection") -> typing.Optional[str]:
+        if self.fact_id_for_caching is not None:
+            is_exist, val = _validator_cache.try_get(self.__class__, c.host, self.fact_id_for_caching)
+            if is_exist:
+                return val
+
+        val = None
+        if self.if_err_true_fn(c):
+            val = self.error_message
+
+        if self.fact_id_for_caching is not None:
+            _validator_cache.set(self.__class__, c.host, self.fact_id_for_caching, val=val)
+        return val
+
+
+class CommandRequiredValidator(StepValidatorBase):
+    def __init__(self, command: str) -> None:
+        self.command = command
+        self.fact_id = f"path-{command}-required"
+
+    def validate(self, c: "Connection") -> typing.Optional[str]:
+        is_exist, val = _validator_cache.try_get(self.__class__, c.host, self.fact_id)
+
+        if is_exist:
+            return val
+
+        val = None
+        if not cmd.cli.is_cmd_exist(c, self.command):
+            val = f"'{self.command}' is required"
+
+        _validator_cache.set(self.__class__, c.host, self.fact_id, val=val)
+        return val
+
+
+class TemplateValidator(StepValidatorBase):
+    def __init__(self, template_path: str, context: typing.Dict[str, typing.Any]):
+        self.template_path = template_path
+        self.context = context
+
+    def validate(self, c: "Connection") -> typing.Optional[str]:
+        # TODO: more catch errors maybe?
+        try:
+            render(self.template_path, **self.context)
+        except (UndefinedError, TemplateNotFound, TemplateSyntaxError) as ex:
+            return f"{ex.__class__.__name__}: {ex}"
+        return None
diff --git a/carnival/task.py b/carnival/task.py
index 2060c73..718ad04 100644
--- a/carnival/task.py
+++ b/carnival/task.py
@@ -5,9 +5,9 @@
 
 from colorama import Fore as F, Style as S, Back as B  # type: ignore
 
-from carnival import Step
-from carnival.role import Role
-from carnival.exceptions import StepValidationError
+if typing.TYPE_CHECKING:
+    from carnival.role import Role
+    from carnival import Step
 
 
 def _underscore(word: str) -> str:
@@ -89,7 +89,7 @@ def run(self) -> None:
         raise NotImplementedError
 
 
-RoleT = typing.TypeVar("RoleT", bound=Role)
+RoleT = typing.TypeVar("RoleT", bound="Role")
 
 
 class Task(abc.ABC, typing.Generic[RoleT], TaskBase):
@@ -118,7 +118,7 @@ def __init__(self, no_validate: bool) -> None:
             print(f"[WARN]: not hosts for {self.role_class}", file=sys.stderr)
 
     @abc.abstractmethod
-    def get_steps(self) -> typing.List[Step]:
+    def get_steps(self) -> typing.List["Step"]:
         """
         Список шагов в порядке выполнения
         """
@@ -134,20 +134,22 @@ def validate(self) -> bool:
         from carnival.cli import carnival_tasks_module
         from carnival.tasks_loader import get_task_full_name
         task_name = get_task_full_name(carnival_tasks_module, self.__class__)
-        print(f"Validating task {S.BRIGHT}{F.BLUE}{task_name}{F.RESET}{S.RESET_ALL}", end="", flush=True)
+        print(f"Validating task {S.BRIGHT}{F.BLUE}{task_name}{F.RESET}{S.RESET_ALL} ", end="", flush=True)
         errors: typing.List[str] = []
 
         for hostrole in self.hostroles:
             with hostrole.host.connect() as c:
                 self.role = hostrole
                 for step in self.get_steps():
-                    try:
-                        step.validate(c=c)
+                    step_errors = step.validate(c=c)
+
+                    if not step_errors:
                         print(f"{F.GREEN}.{F.RESET}", end="", flush=True)
-                    except StepValidationError as ex:
+                    else:
                         step_name = step.get_name()
-                        errors.append(f"{task_name} -> {step_name} on {hostrole.host}: {F.RED}{ex}{F.RESET}")
-                        print("{Fore.RED}e{Fore.RESET}", end="", flush=True)
+                        for e in step_errors:
+                            errors.append(f"{task_name} -> {step_name} on {hostrole.host}: {F.RED}{e}{F.RESET}")
+                        print(f"{F.RED}e{F.RESET}", end="", flush=True)
                 del self.role
 
         if errors:
diff --git a/carnival/templates.py b/carnival/templates.py
index 6a48a3a..ed7dce7 100644
--- a/carnival/templates.py
+++ b/carnival/templates.py
@@ -9,10 +9,18 @@
     PrefixLoader,
 )
 from jinja2.runtime import StrictUndefined
-from jinja2.exceptions import UndefinedError
+from jinja2.exceptions import UndefinedError, TemplateSyntaxError
 
 from carnival.plugins import discover_plugins
 
+
+def escape_yaml(data: str) -> str:
+    """
+    Jinja2 filter for escape yaml dollar sign
+    """
+    return data.replace("$", "$$")
+
+
 """
 Initialize loader on current working dir and plugin modules
 """
@@ -21,10 +29,14 @@
         FileSystemLoader(os.getcwd()),
         PrefixLoader({x: PackageLoader(x, package_path="") for x in discover_plugins().keys()}),
     ]),
+    keep_trailing_newline=True,
     undefined=StrictUndefined,
 )
 
 
+j2_env.filters['escape_yaml'] = escape_yaml
+
+
 def render(template_path: str, **context: Any) -> str:
     """
     Отрендерить jinja2-шаблон в строку
@@ -36,4 +48,10 @@ def render(template_path: str, **context: Any) -> str:
         template = j2_env.get_template(template_path)
         return template.render(**context)
     except UndefinedError as ex:
-        raise UndefinedError(f"Can't render template {template_path}: {ex}")
+        raise UndefinedError(f"Can't render template {template_path} - {ex}") from ex
+    except TemplateSyntaxError as ex:
+        raise TemplateSyntaxError(
+            message=f"Can't render template {template_path}:{ex.lineno} - {ex.message}",
+            lineno=ex.lineno,
+            filename=ex.filename,
+        ) from ex
diff --git a/carnival_tasks_example.py b/carnival_tasks_example.py
index e966682..9e3de5f 100644
--- a/carnival_tasks_example.py
+++ b/carnival_tasks_example.py
@@ -7,7 +7,6 @@
 
 import os
 from carnival import cmd, TaskBase, SshHost, Step, Task, Connection, Role
-from carnival.exceptions import StepValidationError
 
 
 class PackagesRole(Role):
@@ -34,9 +33,12 @@ def __init__(self, packages: typing.List[str], update: bool = True) -> None:
         self.packages = self.packages.strip()
         self.update = update
 
-    def validate(self, c: Connection) -> None:
+    def validate(self, c: Connection) -> typing.List[str]:
+        errors = []
         if not self.packages:
-            raise StepValidationError("packages cant be empty!")
+            errors.append("packages cant be empty!")
+
+        return errors
 
     def run(self, c: Connection) -> None:
         if self.update:
diff --git a/tests/test_utils.py b/tests/test_utils.py
index 4518f0b..e69de29 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -1,7 +0,0 @@
-from carnival import utils, LocalHost
-
-
-def test_log(capsys):
-    utils.log("Hellotest", host=LocalHost())
-    captured = capsys.readouterr()
-    assert captured.out == "💃💃💃 🖥 local> Hellotest\n"