diff --git a/src/luamod.rs b/src/luamod.rs
index 463acf276840b3b4b0edab41c4a8dbb9932864a1..9c37830e1122be8759ed95491721007c6bbf0630 100644
--- a/src/luamod.rs
+++ b/src/luamod.rs
@@ -637,21 +637,26 @@ pub(crate) fn setup(args: &args::Run) {
         &l,
         "cas",
         indoc! {"
-        pico.cas(dml, predicate)
+        pico.cas(dml, [predicate])
         ========================
 
         Performs a clusterwide compare and swap operation.
        
         E.g. it checks the `predicate` on leader and if no conflicting entries were found
-        appends the `op` to the raft log and returns its index (number).
+        appends the `op` to the raft log and returns its index (number). If predicate
+        is not supplied, it will be auto generated with `index` and `term` taken from the
+        current instance and with empty `ranges`.
 
         # Params
         1. dml - table. See pico.help(\"Dml\")
         2. predicate - table. See pico.help(\"Predicate\")
         "},
         tlua::function2(
-            |op: op::DmlInLua, predicate: rpc::cas::Predicate| -> traft::Result<RaftIndex> {
+            |op: op::DmlInLua,
+             predicate: Option<rpc::cas::PredicateInLua>|
+             -> traft::Result<RaftIndex> {
                 let op = op::Dml::from_lua_args(op).map_err(traft::error::Error::other)?;
+                let predicate = rpc::cas::Predicate::from_lua_args(predicate.unwrap_or_default())?;
                 let (index, _) = compare_and_swap(op.into(), predicate)?;
                 Ok(index)
             },
@@ -676,10 +681,15 @@ pub(crate) fn setup(args: &args::Run) {
         "Predicate",
         indoc! {"
         Predicate that will be checked by the leader, before accepting the proposed op.
+
         Has these fields:
-        - index (number)
-        - term (number)
-        - ranges (table)
+        - index (optional, number)
+        - term (optional, number)
+        - ranges (optional, table)
+
+        If some fields are not supplied, they will be autogenerated. `index` and `term` taken from the
+        raft state on the instance which sends this operation and `ranges` left as an empty
+        vector.
         "},
     );
     luamod_set(
diff --git a/src/rpc/cas.rs b/src/rpc/cas.rs
index 9736e625bde1e9952c1430fb30194f7937c11fa5..83a0d9388e1e1e79b4358a3299b0445558906593 100644
--- a/src/rpc/cas.rs
+++ b/src/rpc/cas.rs
@@ -1,6 +1,7 @@
 use crate::storage::Clusterwide;
 use crate::storage::ClusterwideSpaceId;
 use crate::tlog;
+use crate::traft;
 use crate::traft::error::Error as TraftError;
 use crate::traft::node;
 use crate::traft::op::{Ddl, Dml, Op};
@@ -215,8 +216,22 @@ pub enum Error {
     KeyTypeMismatch(#[from] TntError),
 }
 
+/// Represents a lua table describing a [`Predicate`].
+///
+/// This is only used to parse lua arguments from lua api functions such as
+/// `pico.cas`.
+#[derive(Clone, Debug, Default, ::serde::Serialize, ::serde::Deserialize, tlua::LuaRead)]
+pub struct PredicateInLua {
+    /// CaS sender's current raft index.
+    pub index: Option<RaftIndex>,
+    /// CaS sender's current raft term.
+    pub term: Option<RaftTerm>,
+    /// Range that the CaS sender have read and expects it to be unmodified.
+    pub ranges: Option<Vec<Range>>,
+}
+
 /// Predicate that will be checked by the leader, before accepting the proposed `op`.
-#[derive(Clone, Debug, ::serde::Serialize, ::serde::Deserialize, tlua::LuaRead)]
+#[derive(Clone, Debug, ::serde::Serialize, ::serde::Deserialize)]
 pub struct Predicate {
     /// CaS sender's current raft index.
     pub index: RaftIndex,
@@ -227,6 +242,27 @@ pub struct Predicate {
 }
 
 impl Predicate {
+    pub fn from_lua_args(predicate: PredicateInLua) -> traft::Result<Self> {
+        let node = traft::node::global()?;
+        let (index, term) = if let Some(index) = predicate.index {
+            if let Some(term) = predicate.term {
+                (index, term)
+            } else {
+                let term = raft::Storage::term(&node.raft_storage, index)?;
+                (index, term)
+            }
+        } else {
+            let index = node.get_index();
+            let term = raft::Storage::term(&node.raft_storage, index)?;
+            (index, term)
+        };
+        Ok(Self {
+            index,
+            term,
+            ranges: predicate.ranges.unwrap_or_default(),
+        })
+    }
+
     /// Checks if `entry_op` changes anything within the ranges specified in the predicate.
     pub fn check_entry(
         &self,
diff --git a/test/conftest.py b/test/conftest.py
index 3b53ccc868945f64c412b2483e8fa3274c2c9e7b..bd77f02e063e3c652d79fb59bcb26e144155f61e 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -607,6 +607,21 @@ class Instance:
 
         return self.call("pico.raft_compact_log", 0)
 
+    def space_id(self, space: str | int) -> int:
+        """
+        Get space id by space name.
+        If id is supplied instead it is just returned back.
+
+        This method is useful in functions which can take both id and space name.
+        """
+        match space:
+            case int():
+                return space
+            case str():
+                return self.eval("return box.space[...].id", space)
+            case _:
+                raise TypeError("space must be str or int")
+
     def cas(
         self,
         dml_kind: Literal["insert", "replace", "delete"],
@@ -614,7 +629,7 @@ class Instance:
         tuple: Tuple | List,
         index: int | None = None,
         term: int | None = None,
-        range: CasRange | None = None,  # TODO better types for bounds
+        range: CasRange | None = None,
     ) -> int:
         """
         Performs a clusterwide compare and swap operation.
@@ -622,13 +637,6 @@ class Instance:
         E.g. it checks the `predicate` on leader and if no conflicting entries were found
         appends the `op` to the raft log and returns its index.
 
-        `range` is a tuple of two dictionaries: (key_min, key_max). Each dictionary
-        is of the following structure:
-        {
-            "kind": "included" | "excluded" | "unbounded",
-            "value": int | None
-        }
-
         ASSUMPTIONS
         It is assumed that this operation is called on leader.
         Failing to do so will result in an error.
@@ -639,14 +647,7 @@ class Instance:
         elif term is None:
             term = self.raft_term_by_index(index)
 
-        space_id = None
-        match space:
-            case int():
-                space_id = space
-            case str():
-                space_id = self.eval("return box.space[...].id", space)
-            case _:
-                raise TypeError("space must be str or int")
+        space_id = self.space_id(space)
 
         predicate_range = None
         if range is not None:
@@ -1077,7 +1078,7 @@ class Cluster:
         tuple: Tuple | List,
         index: int | None = None,
         term: int | None = None,
-        range: CasRange | None = None,  # TODO better types
+        range: CasRange | None = None,
         # If specified send CaS through this instance
         instance: Instance | None = None,
     ) -> int:
@@ -1088,21 +1089,9 @@ class Cluster:
         appends the `op` to the raft log and returns its index.
 
         Calling this operation will route CaS request to a leader.
-
-        `range` is a tuple of two dictionaries: (key_min, key_max). Each dictionary
-        is of the following structure:
-        {
-            "kind": "included" | "excluded" | "unbounded",
-            "value": int  # skip if `None`
-        }
         """
         if instance is None:
             instance = self.instances[0]
-        if index is None:
-            index = instance.raft_read_index()
-            term = instance.raft_term_by_index(index)
-        elif term is None:
-            term = instance.raft_term_by_index(index)
 
         predicate_range = None
         if range is not None:
@@ -1115,7 +1104,7 @@ class Cluster:
         predicate = dict(
             index=index,
             term=term,
-            ranges=[predicate_range] if predicate_range is not None else [],
+            ranges=predicate_range,
         )
         if dml_kind in ["insert", "replace", "delete"]:
             dml = dict(