diff --git a/doc/sql/query.ebnf b/doc/sql/query.ebnf
index c651b41e49925759420e90997c2d7b1d1585bc3a..388dfdd1d0552c58a17ab4a25b0c68628583ebcf 100644
--- a/doc/sql/query.ebnf
+++ b/doc/sql/query.ebnf
@@ -1,9 +1,10 @@
-statement   ::= explain | ddl | dml | dql | acl
+statement   ::= explain | ddl | dml | dql | acl | call
 explain     ::= 'EXPLAIN' ( dml | dql )
 dql         ::= (select | select union all select | select (except 'DISTINCT'? ) select ) options?
 dml         ::= (delete | insert | update) options?
 ddl         ::= create_table | drop_table | create_procedure | drop_procedure
 acl         ::= drop_role | drop_user | create_role | create_user | alter_user | grant_privilege | revoke_privilege
+call        ::= 'CALL' procedure '(' value ( ',' value )* ')' options?
 revoke_privilege      ::= 'REVOKE' privilege 'FROM' (role | user) ('OPTION' '(' ('TIMEOUT' '=' double)')')?
 grant_privilege       ::= 'GRANT' privilege 'TO' (role | user) ('OPTION' '(' ('TIMEOUT' '=' double)')')?
 privilege ::= (('CREATE' | 'ALTER' | 'DROP') 'USER')
diff --git a/sbroad-cartridge/src/api/exec_query.rs b/sbroad-cartridge/src/api/exec_query.rs
index dc30c0c4f00102e00013609be2cd9a10d1de5ed0..c59f7c0462af992d8ccaa2df0283845c6b3742d9 100644
--- a/sbroad-cartridge/src/api/exec_query.rs
+++ b/sbroad-cartridge/src/api/exec_query.rs
@@ -51,6 +51,9 @@ pub extern "C" fn dispatch_query(f_ctx: FunctionCtx, args: FunctionArgs) -> c_in
         if let Ok(true) = query.is_acl() {
             return tarantool_error("ACL queries are not supported");
         }
+        if let Ok(true) = query.is_block() {
+            return tarantool_error("Blocks of commands are not supported");
+        }
 
         match query.dispatch() {
             Ok(result) => child_span("\"tarantool.tuple.return\"", || {
diff --git a/sbroad-cartridge/test_app/test/integration/block_test.lua b/sbroad-cartridge/test_app/test/integration/block_test.lua
new file mode 100644
index 0000000000000000000000000000000000000000..0d1a7fa573ead40c8e21d40018d1ec9c85fe5bcf
--- /dev/null
+++ b/sbroad-cartridge/test_app/test/integration/block_test.lua
@@ -0,0 +1,31 @@
+local t = require('luatest')
+local g = t.group('sbroad_with_block')
+
+local helper = require('test.helper.cluster_no_replication')
+local cluster = nil
+
+g.before_all(
+        function()
+            helper.start_test_cluster(helper.cluster_config)
+            cluster = helper.cluster
+        end
+)
+
+g.after_all(
+        function()
+            helper.stop_test_cluster()
+        end
+)
+
+g.test_call_proc = function()
+    local api = cluster:server("api-1").net_box
+
+    local _, err = api:call(
+            "sbroad.execute",
+            { [[ call proc() ]], {} }
+    )
+    t.assert_equals(
+            string.format("%s", err),
+            [[Sbroad Error: Blocks of commands are not supported]]
+    )
+end
diff --git a/sbroad-core/src/executor.rs b/sbroad-core/src/executor.rs
index cb9ccca14e9ad70b5d7b6c423b38c4757f6b967f..cd5533a80a9fbb75ec7c9c9bacff2de0c145c87c 100644
--- a/sbroad-core/src/executor.rs
+++ b/sbroad-core/src/executor.rs
@@ -141,11 +141,13 @@ where
                 }
                 plan.version_map = table_version_map;
             }
-            if !plan.is_ddl()? && !plan.is_acl()? && !plan.is_block()? {
+            if !plan.is_ddl()? && !plan.is_acl()? {
                 cache.put(key, plan.clone())?;
             }
         }
-        if !plan.is_ddl()? && !plan.is_acl()? && !plan.is_block()? {
+        if plan.is_block()? {
+            plan.bind_params(params)?;
+        } else if !plan.is_ddl()? && !plan.is_acl()? {
             plan.bind_params(params)?;
             plan.apply_options()?;
             plan.optimize()?;
diff --git a/sbroad-core/src/frontend/sql.rs b/sbroad-core/src/frontend/sql.rs
index 99c5de8dd31584064f79aeec2463f4f085bb0ae4..3caf1ec09c8564e3639f8e49eb56608d63bc153d 100644
--- a/sbroad-core/src/frontend/sql.rs
+++ b/sbroad-core/src/frontend/sql.rs
@@ -25,13 +25,14 @@ use crate::ir::operator::{Arithmetic, Bool, ConflictStrategy, JoinKind, Relation
 use crate::ir::relation::{Column, ColumnRole, Type as RelationType};
 use crate::ir::tree::traversal::PostOrder;
 use crate::ir::value::Value;
-use crate::ir::{Node, OptionKind, OptionSpec, Plan};
+use crate::ir::{Node, NodeId, OptionKind, OptionSpec, Plan};
 use crate::otm::child_span;
 
 use crate::errors::Entity::AST;
 use crate::ir::acl::AlterOption;
 use crate::ir::acl::{Acl, GrantRevokeType, Privilege};
 use crate::ir::aggregates::AggregateKind;
+use crate::ir::block::Block;
 use crate::ir::helpers::RepeatableState;
 use crate::ir::transformation::redistribution::ColumnPosition;
 use sbroad_proc::otm_child_span;
@@ -158,6 +159,43 @@ fn parse_proc_params(
     Ok(params)
 }
 
+fn parse_call_proc(
+    ast: &AbstractSyntaxTree,
+    node: &ParseNode,
+    map: &Translation,
+) -> Result<Block, SbroadError> {
+    if node.rule != Type::CallProc {
+        return Err(SbroadError::Invalid(
+            Entity::Type,
+            Some("call procedure".into()),
+        ));
+    }
+    let mut proc_name: String = String::new();
+    let mut values: Vec<NodeId> = Vec::new();
+    for child_id in &node.children {
+        let child_node = ast.nodes.get_node(*child_id)?;
+        match child_node.rule {
+            Type::ProcName => {
+                proc_name = normalize_name_for_space_api(parse_string_value_node(ast, *child_id)?);
+            }
+            Type::ProcValues => {
+                let values_node = ast.nodes.get_node(*child_id)?;
+                values.reserve(values_node.children.len());
+                for value_id in &values_node.children {
+                    let plan_value_id = map.get(*value_id)?;
+                    values.push(plan_value_id);
+                }
+            }
+            _ => panic!("Unexpected node: {child_node:?}"),
+        }
+    }
+    let call_proc = Block::Procedure {
+        name: proc_name,
+        values,
+    };
+    Ok(call_proc)
+}
+
 fn parse_create_proc(ast: &AbstractSyntaxTree, node: &ParseNode) -> Result<Ddl, SbroadError> {
     if node.rule != Type::CreateProc {
         return Err(SbroadError::Invalid(
@@ -173,7 +211,7 @@ fn parse_create_proc(ast: &AbstractSyntaxTree, node: &ParseNode) -> Result<Ddl,
     for child_id in &node.children {
         let child_node = ast.nodes.get_node(*child_id)?;
         match child_node.rule {
-            Type::NewProc => {
+            Type::ProcName => {
                 proc_name = normalize_name_for_space_api(parse_string_value_node(ast, *child_id)?);
             }
             Type::ProcParams => {
@@ -2143,6 +2181,19 @@ impl Ast for AbstractSyntaxTree {
                         map.get(*node.children.first().expect("no children for Query rule"))?;
                     map.add(id, child_id);
                 }
+                Type::Block => {
+                    // Query may have two children:
+                    // 1. call
+                    // 2. Option child - for which no plan node is created
+                    let child_id =
+                        map.get(*node.children.first().expect("no children for Block rule"))?;
+                    map.add(id, child_id);
+                }
+                Type::CallProc => {
+                    let call_proc = parse_call_proc(self, node, &map)?;
+                    let plan_id = plan.nodes.push(Node::Block(call_proc));
+                    map.add(id, plan_id);
+                }
                 Type::CreateProc => {
                     let create_proc = parse_create_proc(self, node)?;
                     let plan_id = plan.nodes.push(Node::Ddl(create_proc));
@@ -2421,7 +2472,6 @@ impl Ast for AbstractSyntaxTree {
                 | Type::Md5
                 | Type::Memtx
                 | Type::Multiply
-                | Type::NewProc
                 | Type::NewTable
                 | Type::NotEq
                 | Type::Name
@@ -2449,8 +2499,9 @@ impl Ast for AbstractSyntaxTree {
                 | Type::ProcBody
                 | Type::ProcParamDef
                 | Type::ProcParams
-                | Type::ProcLanguage
                 | Type::ProcName
+                | Type::ProcValues
+                | Type::ProcLanguage
                 | Type::ProcWithOptionalParams
                 | Type::ScanName
                 | Type::Select
diff --git a/sbroad-core/src/frontend/sql/ast.rs b/sbroad-core/src/frontend/sql/ast.rs
index 8d108a8b5352ac0e5d314cb4264d7f38c5e63b0f..e40dece2c846d5ffc614280d2fbe36ea17c30593 100644
--- a/sbroad-core/src/frontend/sql/ast.rs
+++ b/sbroad-core/src/frontend/sql/ast.rs
@@ -40,9 +40,11 @@ pub enum Type {
     Asterisk,
     AuthMethod,
     Between,
+    Block,
     Cast,
     ChapSha1,
     Cmp,
+    CallProc,
     CreateProc,
     CreateTable,
     Column,
@@ -103,7 +105,6 @@ pub enum Type {
     Multiplication,
     Multiply,
     Name,
-    NewProc,
     NewTable,
     Not,
     NotEq,
@@ -142,6 +143,7 @@ pub enum Type {
     ProcParams,
     ProcParamDef,
     ProcWithOptionalParams,
+    ProcValues,
     Projection,
     Reference,
     RevokePrivilege,
@@ -206,9 +208,11 @@ impl Type {
             Rule::Asterisk => Ok(Type::Asterisk),
             Rule::AuthMethod => Ok(Type::AuthMethod),
             Rule::Between => Ok(Type::Between),
+            Rule::Block => Ok(Type::Block),
             Rule::Cast => Ok(Type::Cast),
             Rule::ChapSha1 => Ok(Type::ChapSha1),
             Rule::Cmp => Ok(Type::Cmp),
+            Rule::CallProc => Ok(Type::CallProc),
             Rule::CreateProc => Ok(Type::CreateProc),
             Rule::CreateRole => Ok(Type::CreateRole),
             Rule::CreateTable => Ok(Type::CreateTable),
@@ -269,7 +273,6 @@ impl Type {
             Rule::Multiplication => Ok(Type::Multiplication),
             Rule::Multiply => Ok(Type::Multiply),
             Rule::Name => Ok(Type::Name),
-            Rule::NewProc => Ok(Type::NewProc),
             Rule::NewTable => Ok(Type::NewTable),
             Rule::Not => Ok(Type::Not),
             Rule::NotEq => Ok(Type::NotEq),
@@ -304,10 +307,11 @@ impl Type {
             Rule::PrivilegeWrite => Ok(Type::PrivilegeWrite),
             Rule::ProcBody => Ok(Type::ProcBody),
             Rule::ProcLanguage => Ok(Type::ProcLanguage),
-            Rule::ProcName => Ok(Type::ProcName),
             Rule::ProcParams => Ok(Type::ProcParams),
             Rule::ProcParamDef => Ok(Type::ProcParamDef),
             Rule::ProcWithOptionalParams => Ok(Type::ProcWithOptionalParams),
+            Rule::ProcName => Ok(Type::ProcName),
+            Rule::ProcValues => Ok(Type::ProcValues),
             Rule::Projection => Ok(Type::Projection),
             Rule::Reference => Ok(Type::Reference),
             Rule::RevokePrivilege => Ok(Type::RevokePrivilege),
diff --git a/sbroad-core/src/frontend/sql/query.pest b/sbroad-core/src/frontend/sql/query.pest
index 50834bfedfa2fa456734bbe2dc1ca0302bf4e296..7ab5fd941047ab1890cdca781912c20761cd7bae 100644
--- a/sbroad-core/src/frontend/sql/query.pest
+++ b/sbroad-core/src/frontend/sql/query.pest
@@ -1,4 +1,4 @@
-Command = _{ SOI ~ (Query | ExplainQuery | DDL | ACL) ~ EOF }
+Command = _{ SOI ~ (Query | ExplainQuery | Block | DDL | ACL) ~ EOF }
 
 ACL = _{ DropRole | DropUser | CreateRole | CreateUser | AlterUser | GrantPrivilege | RevokePrivilege }
     /// Represents both RoleName and UserName.
@@ -78,11 +78,11 @@ DDL = _{ CreateTable | DropTable | CreateProc | DropProc }
     	DeletedTable = @{ Table }
 
     CreateProc = {
-        ^"create" ~ ^"procedure" ~ NewProc ~
+        ^"create" ~ ^"procedure" ~ ProcName ~
         "(" ~ ProcParams? ~ ")" ~ (^"language" ~ ProcLanguage)? ~
         ((^"as" ~ "$$" ~ ProcBody ~ "$$") | (^"begin" ~ "atomic" ~ ProcBody ~ "end"))
     }
-        NewProc = @{ Name }
+        ProcName = @{ Name }
         ProcParams = { ProcParamDef ~ ("," ~ ProcParamDef)* }
         ProcParamDef = { Type }
         ProcLanguage = { SQL }
@@ -91,7 +91,10 @@ DDL = _{ CreateTable | DropTable | CreateProc | DropProc }
 
     DropProc = { ^"drop" ~ ^"procedure" ~ ProcWithOptionalParams ~ Option? }
         ProcWithOptionalParams = { ProcName ~ ("(" ~ ProcParams ~ ")")? }
-        ProcName = @{ Name }
+
+Block = { CallProc ~ Option? }
+    CallProc = { ^"call" ~ ProcName ~ "(" ~ ProcValues ~ ")" }
+        ProcValues = { (Value | Parameter)? ~ ("," ~ (Value | Parameter))* }
 
 ExplainQuery = _{ Explain }
     Explain = { ^"explain" ~ Query }
diff --git a/sbroad-core/src/ir/api/parameter.rs b/sbroad-core/src/ir/api/parameter.rs
index 06830993442e50b6a2e7488c1c1941785a7f0cd2..f61112f5ef0834688dc4c28df99326e385e88eb4 100644
--- a/sbroad-core/src/ir/api/parameter.rs
+++ b/sbroad-core/src/ir/api/parameter.rs
@@ -1,4 +1,5 @@
 use crate::errors::{Entity, SbroadError};
+use crate::ir::block::Block;
 use crate::ir::expression::Expression;
 use crate::ir::operator::Relational;
 use crate::ir::tree::traversal::PostOrder;
@@ -247,7 +248,18 @@ impl Plan {
                     | Expression::Constant { .. }
                     | Expression::CountAsterisk => {}
                 },
-                Node::Parameter | Node::Ddl(..) | Node::Acl(..) | Node::Block(..) => {}
+                Node::Block(block) => match block {
+                    Block::Procedure { ref values, .. } => {
+                        for param_id in values {
+                            if param_node_ids.take(param_id).is_some() {
+                                // We don't need to wrap arguments, passed into the
+                                // procedure call, into the rows.
+                                idx = idx.saturating_sub(1);
+                            }
+                        }
+                    }
+                },
+                Node::Parameter | Node::Ddl(..) | Node::Acl(..) => {}
             }
         }
 
@@ -360,7 +372,18 @@ impl Plan {
                     | Expression::Constant { .. }
                     | Expression::CountAsterisk => {}
                 },
-                Node::Parameter | Node::Ddl(..) | Node::Acl(..) | Node::Block(..) => {}
+                Node::Block(block) => match block {
+                    Block::Procedure { ref mut values, .. } => {
+                        for param_id in values {
+                            if param_node_ids_cloned.take(param_id).is_some() {
+                                idx = idx.saturating_sub(1);
+                                let val_id = get_value(*param_id, idx)?;
+                                *param_id = val_id;
+                            }
+                        }
+                    }
+                },
+                Node::Parameter | Node::Ddl(..) | Node::Acl(..) => {}
             }
         }
 
diff --git a/sbroad-core/src/ir/block.rs b/sbroad-core/src/ir/block.rs
index be8d4e140bbf04003ab149dedbb41647dfad1557..29ecb0c77c9e198a9b5663cf4f4e08d6516d10b7 100644
--- a/sbroad-core/src/ir/block.rs
+++ b/sbroad-core/src/ir/block.rs
@@ -1,6 +1,7 @@
+//! IR nodes representing blocks of commands.
+
 use crate::errors::{Entity, SbroadError};
 use crate::ir::{Node, NodeId, Plan};
-
 use serde::{Deserialize, Serialize};
 
 #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
diff --git a/sbroad-core/src/ir/relation.rs b/sbroad-core/src/ir/relation.rs
index 1bc1723e607ca3ff6ab77900171dcb3f036158b8..51fa2069fa978394a7fc465f37638e75e58d4a9c 100644
--- a/sbroad-core/src/ir/relation.rs
+++ b/sbroad-core/src/ir/relation.rs
@@ -91,6 +91,33 @@ impl From<&Type> for SpaceFieldType {
     }
 }
 
+impl TryFrom<SpaceFieldType> for Type {
+    type Error = SbroadError;
+
+    fn try_from(field_type: SpaceFieldType) -> Result<Self, Self::Error> {
+        match field_type {
+            SpaceFieldType::Boolean => Ok(Type::Boolean),
+            SpaceFieldType::Decimal => Ok(Type::Decimal),
+            SpaceFieldType::Double => Ok(Type::Double),
+            SpaceFieldType::Integer => Ok(Type::Integer),
+            SpaceFieldType::Number => Ok(Type::Number),
+            SpaceFieldType::Scalar => Ok(Type::Scalar),
+            SpaceFieldType::String => Ok(Type::String),
+            SpaceFieldType::Unsigned => Ok(Type::Unsigned),
+            SpaceFieldType::Array => Ok(Type::Array),
+            SpaceFieldType::Any
+            | SpaceFieldType::Varbinary
+            | SpaceFieldType::Uuid
+            | SpaceFieldType::Map
+            | SpaceFieldType::Interval
+            | SpaceFieldType::Datetime => Err(SbroadError::NotImplemented(
+                Entity::Type,
+                field_type.to_string(),
+            )),
+        }
+    }
+}
+
 impl Type {
     /// Type constructor
     ///
@@ -141,6 +168,22 @@ impl Type {
                 | Type::Unsigned
         )
     }
+
+    /// Check if the type can be casted to another type.
+    #[must_use]
+    pub fn is_castable_to(&self, to: &Type) -> bool {
+        matches!(
+            (self, to),
+            (Type::Array, Type::Array)
+                | (Type::Boolean, Type::Boolean)
+                | (
+                    Type::Double | Type::Integer | Type::Unsigned | Type::Decimal | Type::Number,
+                    Type::Double | Type::Integer | Type::Unsigned | Type::Decimal | Type::Number,
+                )
+                | (Type::Scalar, Type::Scalar)
+                | (Type::String, Type::String)
+        )
+    }
 }
 
 /// A role of the column in the relation.