diff --git a/app-openbis-command-line/src/python/obis/dm/commands/download_physical.py b/app-openbis-command-line/src/python/obis/dm/commands/download_physical.py
index 44845cf12abe87dfa2f7e1823c0424cc679cc1c2..1b5e94c4878e2d35613278acaa94125ba492e74e 100644
--- a/app-openbis-command-line/src/python/obis/dm/commands/download_physical.py
+++ b/app-openbis-command-line/src/python/obis/dm/commands/download_physical.py
@@ -12,6 +12,7 @@
 #   See the License for the specific language governing permissions and
 #   limitations under the License.
 #
+import csv
 import os
 
 from .openbis_command import OpenbisCommand
@@ -26,12 +27,16 @@ class DownloadPhysical(OpenbisCommand):
     Command to download physical files of a data set.
     """
 
-    def __init__(self, dm, data_set_id, file, skip_integrity_check):
+    def __init__(self, dm, data_set_id, from_file, file, skip_integrity_check):
         """
-        :param dm: data management
-        :param data_set_id: permId of the data set to be cloned
+        :param dm: data management.
+        :param data_set_id: permId of the data set to be cloned.
+        :param from_file: Path to a CSV file with a list of datasets to download.
+        :param file: path to a specific file to download from a dataset.
+        :param skip_integrity_check: boolean flag indicating whether to skip checksum validation.
         """
         self.data_set_id = data_set_id
+        self.from_file = from_file
         self.files = [file] if file is not None else None
         self.skip_integrity_check = skip_integrity_check
         self.load_global_config(dm)
@@ -42,25 +47,37 @@ class DownloadPhysical(OpenbisCommand):
             return CommandResult(returncode=-1,
                                  output="Configuration fileservice_url needs to be set for download.")
 
-        data_set = self.openbis.get_dataset(self.data_set_id)
-        files = self.files if self.files is not None else data_set.file_list
+        if self.from_file is not None:
+            with cd(self.data_mgmt.invocation_path):
+                with open(self.from_file, newline='') as csvfile:
+                    reader = csv.DictReader(csvfile)
+                    for row in reader:
+                        self.download_dataset(row['permId'])
+                    if reader.line_num == 1:  # First row contains headers
+                        click_echo("No data sets were found in provided file!")
+        else:
+            self.download_dataset(self.data_set_id)
+        return CommandResult(returncode=0, output="Download completed.")
 
+    def download_dataset(self, perm_id):
+        data_set = self.openbis.get_dataset(perm_id)
+        files = self.files if self.files is not None else data_set.file_list
         with cd(self.data_mgmt.invocation_path):
             target_folder = data_set.download(files, destination=self.data_mgmt.invocation_path)
             if self.skip_integrity_check is not True:
                 invalid_files = validate_checksum(self.openbis, files, data_set.permId,
                                                   target_folder, None)
-                self.redownload_invalid_files_on_demand(invalid_files, target_folder)
-            return CommandResult(returncode=0, output="Files downloaded to: %s" % target_folder)
+                self.redownload_invalid_files_on_demand(invalid_files, target_folder, perm_id)
+                click_echo(f"Files from dataset {perm_id} has been downloaded to {target_folder}")
 
-    def redownload_invalid_files_on_demand(self, invalid_files, target_folder):
+    def redownload_invalid_files_on_demand(self, invalid_files, target_folder, perm_id):
         if len(invalid_files) == 0:
             return
         yes_or_no = None
         while yes_or_no != "yes" and yes_or_no != "no":
-            click_echo("Integrity check failed for following files:\n" +
+            click_echo(f"Integrity check failed for following files in dataset {perm_id}:\n" +
                        str(invalid_files) + "\n" +
-                       "Either the download failed or the files where changed after committing to openBIS.\n" +
+                       "Either the download failed or the files where changed in the OpenBIS.\n" +
                        "Should the files be downloaded again? (yes/no)")
             yes_or_no = input('> ')
         if yes_or_no == "yes":
@@ -68,4 +85,4 @@ class DownloadPhysical(OpenbisCommand):
                 filename_dest = os.path.join(target_folder, file)
                 os.remove(filename_dest)
             self.files = invalid_files
-            return self.run()
+            return self.download_dataset(perm_id)
diff --git a/app-openbis-command-line/src/python/obis/dm/commands/search.py b/app-openbis-command-line/src/python/obis/dm/commands/search.py
index 4ef0d1e865fa5a972fce32d10f8f0a2078b061ed..d4b2ad454ed0b47011402a553240b5977f78c67b 100644
--- a/app-openbis-command-line/src/python/obis/dm/commands/search.py
+++ b/app-openbis-command-line/src/python/obis/dm/commands/search.py
@@ -13,8 +13,6 @@
 #   limitations under the License.
 #
 
-import os
-
 from .openbis_command import OpenbisCommand
 from ..command_result import CommandResult
 from ..utils import cd
@@ -84,7 +82,7 @@ class Search(OpenbisCommand):
                 self.property_code: self.property_value,
             }
 
-        search_results = self.openbis.get_samples(
+        datasets = self.openbis.get_datasets(
             space=self.space,
             project=self.project,  # Not Supported with Project Samples disabled
             experiment=self.experiment,
@@ -94,43 +92,11 @@ class Search(OpenbisCommand):
             props="*"  # Fetch all properties
         )
 
-        collections = self.openbis.get_collections(
-            space=self.space,
-            project=self.project,
-            type=self.type_code,
-            where=properties,
-            props="*"  # Fetch all properties
-        )
-
-        click_echo("Looking for data sets")
-        datasets = []
-        perm_ids = set()
-        for sample in search_results:
-            ds = sample.get_datasets()
-            for ds_object in ds.objects:
-                datasets += [ds_object] if ds_object.permId not in perm_ids else []
-                perm_ids.add(ds_object.permId)
-        for collection in collections:
-            ds = collection.get_datasets()
-            for ds_object in ds.objects:
-                datasets += [ds_object] if ds_object.permId not in perm_ids else []
-                perm_ids.add(ds_object.permId)
-
         click_echo(f"Data sets found: {len(datasets)}")
         if self.save_path is not None:
+            click_echo(f"Saving search results in {self.save_path}")
             with cd(self.data_mgmt.invocation_path):
-                if os.path.exists(self.save_path) is True and os.path.isdir(
-                        self.save_path) is False:
-                    return CommandResult(returncode=-1,
-                                         output=f"File {self.save_path} is not a directory")
-                if os.path.isdir(self.save_path) is False:
-                    click_echo(f"Creating directory {self.save_path}")
-                    os.makedirs(self.save_path)
-                click_echo(
-                    f"Saving search results in {os.path.join(self.data_mgmt.invocation_path, self.save_path)}")
-                for dataset in datasets:
-                    dataset.download(destination=self.save_path,
-                                     linked_dataset_fileservice_url=self.fileservice_url() + "/download")
+                datasets.df.to_csv(self.save_path, index=False)
         else:
             click_echo(f"Search results:\n{datasets}")
 
diff --git a/app-openbis-command-line/src/python/obis/dm/data_mgmt.py b/app-openbis-command-line/src/python/obis/dm/data_mgmt.py
index 92464167e30ff43d178ccf88538fdaf7d15e0cd2..92c5a514662296f08e1d4eae3995624e1f0624f1 100644
--- a/app-openbis-command-line/src/python/obis/dm/data_mgmt.py
+++ b/app-openbis-command-line/src/python/obis/dm/data_mgmt.py
@@ -210,9 +210,10 @@ class AbstractDataMgmt(metaclass=abc.ABCMeta):
         return
 
     @abc.abstractmethod
-    def download(self, data_set_id, file, skip_integrity_check):
+    def download(self, data_set_id, from_file, file, skip_integrity_check):
         """Download files of a repository without adding a content copy.
         :param data_set_id: Id of the data set to download from.
+        :param from_file: Path of a file with a list of datasets to download.
         :param file: Path of a file in the data set to download. All files are downloaded if it is None.
         :param skip_integrity_check: Checksums of files are not verified if true.
         """
@@ -349,7 +350,7 @@ class NoGitDataMgmt(AbstractDataMgmt):
     def removeref(self, data_set_id=None):
         self.error_raise("removeref", "No git command found.")
 
-    def download(self, data_set_id, file, skip_integrity_check):
+    def download(self, *_):
         self.error_raise("download", "No git command found.")
 
     def search_object(self, *_):
@@ -559,7 +560,7 @@ class GitDataMgmt(AbstractDataMgmt):
         cmd = Removeref(self, data_set_id=data_set_id)
         return cmd.run()
 
-    def download(self, data_set_id, file, skip_integrity_check):
+    def download(self, data_set_id, from_file, file, skip_integrity_check):
         self.error_raise("download", "This command is only available for Manager Data.")
 
     #
@@ -645,8 +646,8 @@ class PhysicalDataMgmt(AbstractDataMgmt):
     def removeref(self, data_set_id=None):
         self.error_raise("removeref", "This command is only available for External Manager Data")
 
-    def download(self, data_set_id, file, skip_integrity_check):
-        cmd = DownloadPhysical(self, data_set_id, file, skip_integrity_check)
+    def download(self, data_set_id, from_file, file, skip_integrity_check):
+        cmd = DownloadPhysical(self, data_set_id, from_file, file, skip_integrity_check)
         return cmd.run()
 
     def upload(self, sample_id, data_set_type, files):
diff --git a/app-openbis-command-line/src/python/obis/scripts/cli.py b/app-openbis-command-line/src/python/obis/scripts/cli.py
index be6804e1cc66ecd2c56b5690f95a31fceeb4f1a8..3c626e9762e4f1df9d1da63a14add25f12497598 100644
--- a/app-openbis-command-line/src/python/obis/scripts/cli.py
+++ b/app-openbis-command-line/src/python/obis/scripts/cli.py
@@ -275,6 +275,7 @@ _search_params = [
     click.option('-space', '--space', default=None, help='Space code'),
     click.option('-project', '--project', default=None, help='Full project identification code'),
     click.option('-experiment', '--experiment', default=None, help='Full experiment code'),
+    click.option('-type', '--type', 'type_code', default=None, help='Type code'),
     click.option('-property', 'property_code', default=None, help='Property code'),
     click.option('-property-value', 'property_value', default=None,
                  help='Property value'),
@@ -758,7 +759,10 @@ def removeref(ctx, data_set_id, repository):
 # download
 
 _download_params = [
-    click.argument('data_set_id'),
+    click.argument('data_set_id', required=False),
+    click.option('-from-file', '--from-file', 'from_file',
+                 help='An output .CSV file from `obis data_set search` command with the list of' +
+                      ' objects to download datasets from'),
     click.option(
         '-f', '--file', help='File in the data set to download - downloading all if not given.'),
     click.option('-s', '--skip_integrity_check', default=False, is_flag=True,
@@ -766,22 +770,21 @@ _download_params = [
 ]
 
 
-@data_set.command("download", short_help="Download files of a data set.")
-@add_params(_download_params)
-@click.pass_context
-def data_set_download(ctx, file, data_set_id, skip_integrity_check):
-    return ctx.obj['runner'].run("download",
-                                 lambda dm: dm.download(data_set_id=data_set_id, file=file,
-                                                        skip_integrity_check=skip_integrity_check))
-
-
 @cli.command("download", short_help="Download files of a data set.")
 @add_params(_download_params)
 @click.pass_context
-def download(ctx, file, data_set_id, skip_integrity_check):
+def download(ctx, data_set_id, from_file, file, skip_integrity_check):
+    """ Downloads dataset files from OpenBIS instance.\n
+    DATA_SET    Unique identifier of dataset within OpenBIS instance."""
+    if (data_set_id is None and from_file is None) or (
+            data_set_id is not None and from_file is not None):
+        click_echo("'data_set_id' or 'from_file' must be provided!")
+        return -1
     ctx.obj['runner'] = DataMgmtRunner(ctx.obj, halt_on_error_log=False)
-    ctx.invoke(data_set_download, file=file,
-               data_set_id=data_set_id, skip_integrity_check=skip_integrity_check)
+    return ctx.obj['runner'].run("download",
+                                 lambda dm: dm.download(data_set_id=data_set_id,
+                                                        from_file=from_file, file=file,
+                                                        skip_integrity_check=skip_integrity_check))
 
 
 # upload
@@ -795,20 +798,17 @@ _upload_params = [
 ]
 
 
-@data_set.command("upload", short_help="Upload files to form a data set.")
-@add_params(_upload_params)
-@click.pass_context
-def data_set_upload(ctx, sample_id, data_set_type, files):
-    return ctx.obj['runner'].run("upload",
-                                 lambda dm: dm.upload(sample_id, data_set_type, files))
-
-
 @cli.command("upload", short_help="Upload files to form a data set.")
 @add_params(_upload_params)
 @click.pass_context
-def download(ctx, sample_id, data_set_type, files):
+def upload(ctx, sample_id, data_set_type, files):
+    """ Creates data set under object and upload files to it.\n
+    SAMPLE_ID       Unique identifier an object in OpenBIS.\n
+    DATA_SET_TYPE   Newly created data set type.
+    """
     ctx.obj['runner'] = DataMgmtRunner(ctx.obj, halt_on_error_log=False)
-    ctx.invoke(data_set_upload, files=files, sample_id=sample_id, data_set_type=data_set_type)
+    ctx.obj['runner'].run("upload",
+                          lambda dm: dm.upload(sample_id, data_set_type, files))
 
 
 # clone
diff --git a/docs/app-openbis-command-line/README.md b/docs/app-openbis-command-line/README.md
index 62505db94e28bfce7854a9bac0370995cae98986..4b8b3566a277bbdb91be4ca2ed9fa6bcee055e38 100644
--- a/docs/app-openbis-command-line/README.md
+++ b/docs/app-openbis-command-line/README.md
@@ -204,6 +204,7 @@ Options:
   -space, --space TEXT            Space code
   -project, --project TEXT        Full project identification code
   -experiment, --experiment TEXT  Full experiment code
+  -type, --type TEXT              Type code
   -property TEXT                  Property code
   -property-value TEXT            Property value
   -save, --save TEXT              Directory name to save results
@@ -211,7 +212,7 @@ Options:
 
 With `data_set search` command, obis connects to a configured OpenBIS instance and searches for all
 data sets that fulfill given filtering criteria.
-At least one filtering criteria must be specified. Resulting data set files can be downloaded by
+At least one filtering criteria must be specified. Search results can be downloaded by
 using `save` option.
 
 *Note: Filtering by `-project` may not work when `Project Samples` are disabled in OpenBIS
@@ -221,6 +222,15 @@ configuration.*
 
 ```
 obis download [options] [data_set_id]
+
+Options:
+  -from-file, --from-file TEXT  An output .CSV file from `obis data_set search`
+                                command with the list of objects to download
+                                data sets from
+  -f, --file TEXT               File in the data set to download - downloading
+                                all if not given.
+  -s, --skip_integrity_check    Flag to skip file integrity check with
+                                checksums
 ```
 
 The `download` command downloads, the files of a given data set from the OpenBIS instance specified