diff --git a/beat/core/database.py b/beat/core/database.py
index 2c200742a20d88cd0b5a8173cc40e77ffc9ef6d0..ce60f72eef728cec92e0286a318254655a3d03a8 100644
--- a/beat/core/database.py
+++ b/beat/core/database.py
@@ -102,6 +102,15 @@ class Database(BackendDatabase):
     def __init__(self, prefix, data, dataformat_cache=None):
         super(Database, self).__init__(prefix, data, dataformat_cache)
 
+    def _validate_view(self, view_name):
+        if view_name.find(".") != -1 or view_name.find(os.sep) != -1:
+            self.errors.append(
+                "dataset views are required to sit inside the "
+                "database root folder, but `%s' is either in a "
+                "subdirectory or points to a python module, what is "
+                "unsupported by this version" % (view_name)
+            )
+
     def _load(self, data, dataformat_cache):
         """Loads the database"""
 
@@ -150,7 +159,7 @@ class Database(BackendDatabase):
         self._validate_semantics(dataformat_cache)
 
     def _validate_semantics(self, dataformat_cache):
-        """Validates all sematical aspects of the database"""
+        """Validates all semantical aspects of the database"""
 
         # all protocol names must be unique
         protocol_names = [k["name"] for k in self.data["protocols"]]
@@ -161,7 +170,7 @@ class Database(BackendDatabase):
 
         # all set names within a protocol must be unique
         for protocol in self.data["protocols"]:
-            set_names = [k["name"] for k in protocol["sets"]]
+            set_names = self.set_names(protocol["name"])
             if len(set_names) != len(set(set_names)):
                 self.errors.append(
                     "found different sets with the same name at protocol "
@@ -169,9 +178,9 @@ class Database(BackendDatabase):
                 )
 
             # all outputs must have valid data types
-            for _set in protocol["sets"]:
+            for _, set_ in self.sets(protocol["name"]).items():
 
-                for key, value in _set["outputs"].items():
+                for key, value in set_["outputs"].items():
 
                     if value in self.dataformats:
                         continue
@@ -191,17 +200,16 @@ class Database(BackendDatabase):
                             % (
                                 value,
                                 key,
-                                _set["name"],
+                                set_["name"],
                                 protocol["name"],
                                 "\n".join(dataformat.errors),
                             )
                         )
 
                 # all view names must be relative to the database root path
-                if _set["view"].find(".") != -1 or _set["view"].find(os.sep) != -1:
-                    self.errors.append(
-                        "dataset views are required to sit inside the "
-                        "database root folder, but `%s' is either in a "
-                        "subdirectory or points to a python module, what is "
-                        "unsupported by this version" % (_set["view"],)
-                    )
+                if self.schema_version == 1:
+                    self._validate_view(set_["view"])
+
+            if self.schema_version != 1:
+                for view in protocol["views"].keys():
+                    self._validate_view(view)
diff --git a/beat/core/protocoltemplate.py b/beat/core/protocoltemplate.py
new file mode 100644
index 0000000000000000000000000000000000000000..11574f56ce556c944fd57238b466cd84c4c80aa4
--- /dev/null
+++ b/beat/core/protocoltemplate.py
@@ -0,0 +1,123 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+
+###################################################################################
+#                                                                                 #
+# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/               #
+# Contact: beat.support@idiap.ch                                                  #
+#                                                                                 #
+# Redistribution and use in source and binary forms, with or without              #
+# modification, are permitted provided that the following conditions are met:     #
+#                                                                                 #
+# 1. Redistributions of source code must retain the above copyright notice, this  #
+# list of conditions and the following disclaimer.                                #
+#                                                                                 #
+# 2. Redistributions in binary form must reproduce the above copyright notice,    #
+# this list of conditions and the following disclaimer in the documentation       #
+# and/or other materials provided with the distribution.                          #
+#                                                                                 #
+# 3. Neither the name of the copyright holder nor the names of its contributors   #
+# may be used to endorse or promote products derived from this software without   #
+# specific prior written permission.                                              #
+#                                                                                 #
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND #
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED   #
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE          #
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE    #
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL      #
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR      #
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER      #
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,   #
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE   #
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.            #
+#                                                                                 #
+###################################################################################
+
+
+"""
+================
+protocoltemplate
+================
+
+Validation of protocoltemplate
+
+Forward importing from :py:mod:`beat.backend.python.protocoltemplate`:
+:py:class:`beat.backend.python.protocoltemplate.Storage`
+"""
+
+import six
+
+from . import schema
+
+from beat.backend.python.protocoltemplate import Storage
+from beat.backend.python.protocoltemplate import (
+    ProtocolTemplate as BackendProtocolTemplate,
+)
+
+
+class ProtocolTemplate(BackendProtocolTemplate):
+    """Protocol template define the design of the database.
+
+
+    Parameters:
+
+      prefix (str): Establishes the prefix of your installation.
+
+      data (dict, str): The piece of data representing the protocol templates.
+        It must validate against the schema defined for protocol templates. If a
+        string is passed, it is supposed to be a valid path to protocol template
+        in the designated prefix area.
+
+      dataformat_cache (:py:class:`dict`, Optional): A dictionary mapping
+        dataformat names to loaded dataformats. This parameter is optional and,
+        if passed, may greatly speed-up protocol template loading times as
+        dataformats that are already loaded may be re-used. If you use this
+        parameter, you must guarantee that the cache is refreshed as appropriate
+        in case the underlying dataformats change.
+
+
+    Attributes:
+
+      name (str): The full, valid name of this protocol template
+
+      description (str): The short description string, loaded from the JSON
+        file if one was set.
+
+      documentation (str): The full-length docstring for this object.
+
+      storage (object): A simple object that provides information about file
+        paths for this protocol template
+
+      errors (list): A list containing errors found while loading this
+        protocol template.
+
+      data (dict): The original data for this protocol template, as loaded by
+        our JSON decoder.
+
+    """
+
+    def __init__(self, prefix, data, dataformat_cache=None):
+        super(ProtocolTemplate, self).__init__(prefix, data, dataformat_cache)
+
+    def _load(self, data, dataformat_cache):
+        """Loads the database"""
+
+        self._name = None
+        self.storage = None
+        self.dataformats = {}  # preloaded dataformats
+
+        if isinstance(data, six.string_types):  # user has passed a file pointer
+
+            self._name = data
+            self.storage = Storage(self.prefix, self._name)
+            data = self.storage.json.path
+            if not self.storage.json.exists():
+                self.errors.append(
+                    "Protocol template declaration file not found: %s" % data
+                )
+                return
+
+        # this runs basic validation, including JSON loading if required
+        self.data, self.errors = schema.validate("protocoltemplate", data)
+        if self.errors:
+            return  # don't proceed with the rest of validation
diff --git a/beat/core/schema/__init__.py b/beat/core/schema/__init__.py
index ae809022eb9a8b3ee7f337259770e44b9794cbdd..d182bf356edc2af58ba47bb9edd5918ceff0bbdf 100644
--- a/beat/core/schema/__init__.py
+++ b/beat/core/schema/__init__.py
@@ -94,7 +94,11 @@ def load_schema(schema_name, version=1):
 
     with open(fname, "rb") as f:
         data = f.read().decode()
-        schema = json.loads(data)
+        try:
+            schema = json.loads(data)
+        except json.errors.JSONDecodeError:
+            print("Invalid json:\n {data}".format(data))
+            raise
 
     basedir = os.path.realpath(os.path.dirname(fname))
     resolver = jsonschema.RefResolver("file://" + basedir + "/", schema)
diff --git a/beat/core/schema/database/2.json b/beat/core/schema/database/2.json
new file mode 100644
index 0000000000000000000000000000000000000000..ea20d45ec57865ceb0be70d9477a2c57cc0b9b2f
--- /dev/null
+++ b/beat/core/schema/database/2.json
@@ -0,0 +1,107 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Database descriptor v2",
+  "description": "This schema defines the properties of a version 2 BEAT database",
+
+  "type": "object",
+
+  "properties": {
+
+    "root_folder": {
+      "type": "string",
+      "pattern": "^((file://)?(/[^/]+)+|nfs://[a-z0-9._-]+:(/[^/]+)+)$"
+    },
+
+    "protocols": {
+      "type": "array",
+      "minItems": 1,
+      "uniqueItems": true,
+      "items": { "$ref": "#/definitions/protocol" }
+    },
+
+    "description": { "$ref": "../common/1.json#/definitions/description" },
+
+    "schema_version": { "const": 2 }
+
+  },
+
+  "required": [
+    "root_folder",
+    "protocols",
+    "schema_version"
+  ],
+
+  "additionalProperties": false,
+
+  "definitions": {
+
+    "template_identifier": {
+      "type": "string",
+      "pattern": "^[a-zA-Z0-9_-]+/[0-9]+$"
+    },
+
+    "protocol": {
+      "type": "object",
+      "properties": {
+        "name": { "$ref": "#/definitions/protocol_name" },
+        "template": { "$ref": "#/definitions/template_identifier" },
+        "views": { "$ref": "#/definitions/views" }
+      },
+      "required": ["name", "views", "template"],
+      "additionalProperties": false
+    },
+
+    "protocol_name": {
+      "type": "string",
+      "pattern": "^[a-zA-Z0-9_][\\.a-zA-Z0-9_-]*$"
+    },
+
+    "view_name": {
+      "type": "string",
+      "pattern": "^[a-zA-Z_][a-zA-Z0-9_]*$"
+    },
+
+    "views": {
+      "type": "object",
+      "minProperties": 1,
+      "uniqueItems": true,
+      "patternProperties": {
+        "^[a-zA-Z_][a-zA-Z0-9_]*$": { "$ref": "#/definitions/view" }
+      }
+    },
+
+    "view": {
+      "type": "object",
+      "properties": {
+        "view": { "$ref": "#definitions/view_name" },
+        "parameters": { "$ref": "#/definitions/parameters" }
+      },
+      "additionalProperties": false
+    },
+
+    "parameters": {
+      "type": "object",
+      "patternProperties": {
+        "^[a-zA-Z_][a-zA-Z0-9_-]*$": {
+          "$ref": "#/definitions/parameter_value"
+        }
+      }
+    },
+
+    "parameter_value": {
+      "oneOf": [
+        {
+          "type": "string"
+        },
+        {
+          "type": "number"
+        },
+        {
+          "type": "boolean"
+        }
+      ]
+    }
+
+  }
+
+}
diff --git a/beat/core/schema/protocoltemplate/1.json b/beat/core/schema/protocoltemplate/1.json
new file mode 100644
index 0000000000000000000000000000000000000000..07af41a741ea69f04136dfa669179bdfe0788171
--- /dev/null
+++ b/beat/core/schema/protocoltemplate/1.json
@@ -0,0 +1,58 @@
+{
+  "$schema": "http://json-schema.org/draft-04/schema#",
+  "title": "Database Protocol descriptor",
+  "description": "This schema defines the properties of a BEAT database protocol",
+
+  "type": "object",
+
+  "properties": {
+
+    "description": { "$ref": "../common/1.json#/definitions/description" },
+
+    "schema_version": { "const": 1 },
+
+    "sets": {
+      "type": "array",
+      "minItems": 1,
+      "uniqueItems": true,
+      "items": { "$ref": "#/definitions/set" }
+    }
+
+  },
+
+  "required": [
+    "schema_version", "sets"
+  ],
+
+  "additionalProperties": false,
+
+  "definitions": {
+
+    "identifier": {
+      "type": "string",
+      "pattern": "^[a-zA-Z_][a-zA-Z0-9_-]*$"
+    },
+
+    "set": {
+      "type": "object",
+      "properties": {
+        "name": { "$ref": "#/definitions/identifier" },
+        "outputs": {
+          "type": "object",
+          "patternProperties": {
+            "^[a-zA-Z_][a-zA-Z0-9_-]*$": {
+              "$ref": "../common/1.json#/definitions/reference"
+            }
+          },
+          "minProperties": 1,
+          "uniqueItems": true,
+          "additionalProperties": false
+        }
+      },
+      "required": ["name", "outputs"],
+      "additionalProperties": false
+    }
+
+  }
+
+}
diff --git a/beat/core/scripts/migrate_db_to_v2.py b/beat/core/scripts/migrate_db_to_v2.py
new file mode 100644
index 0000000000000000000000000000000000000000..707eb5d910bf6cb31da99d9bb1e960290ae775b0
--- /dev/null
+++ b/beat/core/scripts/migrate_db_to_v2.py
@@ -0,0 +1,158 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+
+###################################################################################
+#                                                                                 #
+# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/               #
+# Contact: beat.support@idiap.ch                                                  #
+#                                                                                 #
+# Redistribution and use in source and binary forms, with or without              #
+# modification, are permitted provided that the following conditions are met:     #
+#                                                                                 #
+# 1. Redistributions of source code must retain the above copyright notice, this  #
+# list of conditions and the following disclaimer.                                #
+#                                                                                 #
+# 2. Redistributions in binary form must reproduce the above copyright notice,    #
+# this list of conditions and the following disclaimer in the documentation       #
+# and/or other materials provided with the distribution.                          #
+#                                                                                 #
+# 3. Neither the name of the copyright holder nor the names of its contributors   #
+# may be used to endorse or promote products derived from this software without   #
+# specific prior written permission.                                              #
+#                                                                                 #
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND #
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED   #
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE          #
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE    #
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL      #
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR      #
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER      #
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,   #
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE   #
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.            #
+#                                                                                 #
+###################################################################################
+
+"""Migrate a v1 database to v2
+
+Usage:
+  %(prog)s [-v ... | --verbose ...] [--prefix=<path>][-f|--force]
+           <database_identifier>
+  %(prog)s (--help | -h)
+  %(prog)s (--version | -V)
+
+
+Options:
+  -h, --help                 Show this screen
+  -V, --version              Show version
+  -v, --verbose              Increases the output verbosity level
+  -p, --prefix=<path>        Path where the prefix is contained [default: .]
+"""
+
+import os
+import sys
+import copy
+
+from docopt import docopt
+
+from ..version import __version__
+
+from ..database import Database, Storage as DBStorage
+from ..protocoltemplate import ProtocolTemplate, Storage as PTStorage
+from ..utils import setup_logging
+
+
+def main(argv=None):
+    if argv is None:
+        argv = sys.argv[1:]
+
+    prog = os.path.basename(sys.argv[0])
+    completions = dict(prog=prog, version=__version__)
+    args = docopt(
+        __doc__ % completions,
+        argv=argv,
+        options_first=True,
+        version="v%s" % __version__,
+    )
+
+    logger = setup_logging(args["--verbose"], __name__, __name__)
+
+    prefix = args["--prefix"] if args["--prefix"] is not None else "."
+    if not os.path.exists(prefix):
+        logger.error("Prefix not found at: '%s'", prefix)
+        return 1
+
+    database_identifier = args["<database_identifier>"]
+
+    database = Database(prefix, database_identifier)
+
+    if not database.valid:
+        logger.error("Invalid database: '%s'", "\n".join(database.errors))
+        return 1
+
+    if database.schema_version != 1:
+        logger.error("Can't migrate database is not v1")
+        return 1
+
+    db_name, db_version = database_identifier.split("/")
+    new_db_name = f"{db_name}/{int(db_version) + 1}"
+
+    db_storage = DBStorage(prefix, new_db_name)
+    if db_storage.exists():
+        logger.error(f"Database already exists: {new_db_name}")
+        return 1
+
+    database_json = copy.deepcopy(database.data)
+    database_json["schema_version"] = 2
+    database_json["protocols"] = []
+
+    for protocol in database.protocols:
+        sets = database.sets(protocol)
+        set_list = []
+        views = {}
+        for _, set_ in sets.items():
+            views[set_["name"]] = {
+                "view": set_["view"],
+                "parameters": set_.get("parameters", {}),
+            }
+
+            for key in ["template", "view", "parameters"]:
+                if key in set_:
+                    set_.pop(key)
+            set_list.append(set_)
+
+        template = {"schema_version": 1, "sets": set_list}
+
+        pt_name = f"{protocol}/1"
+        pt_storage = PTStorage(prefix, pt_name)
+
+        if pt_storage.exists():
+            logger.info(f"Protocol template already exists: {pt_name}")
+        else:
+            protocol_template = ProtocolTemplate(prefix, template)
+            if not protocol_template.valid:
+                logger.error(
+                    "Invalid protocol created:", "\n".join(protocol_template.errors)
+                )
+                return 1
+            else:
+                protocol_template.write(pt_storage)
+
+        protocol_entry = {"name": protocol, "template": pt_name, "views": views}
+
+        database_json["protocols"].append(protocol_entry)
+
+    new_database = Database(prefix, database_json)
+    if not new_database.valid:
+        logger.error("Invalid database created:", "\n".join(new_database.errors))
+        return 1
+    else:
+        new_database.code = database.code
+        new_database.description = (
+            database.description if database.description is not None else ""
+        )
+        new_database.write(db_storage)
+
+
+if __name__ == "__main__":
+    main()
diff --git a/beat/core/test/prefix/databases/large/2.json b/beat/core/test/prefix/databases/large/2.json
new file mode 100644
index 0000000000000000000000000000000000000000..629eab25e7f362e76b03cb4c87c0c1a7131adb4d
--- /dev/null
+++ b/beat/core/test/prefix/databases/large/2.json
@@ -0,0 +1,27 @@
+{
+    "root_folder": "/tmp/path/not/set",
+    "protocols": [
+        {
+            "name": "large",
+            "template": "large/1",
+            "views": {
+                "data": {
+                    "view": "LargeView",
+                    "parameters": {}
+                }
+            }
+        },
+        {
+            "name": "small",
+            "template": "small/1",
+            "views": {
+                "data": {
+                    "view": "SmallView",
+                    "parameters": {}
+                }
+            }
+        }
+    ],
+    "schema_version": 2,
+    "description": ""
+}
diff --git a/beat/core/test/prefix/databases/large/2.py b/beat/core/test/prefix/databases/large/2.py
new file mode 100644
index 0000000000000000000000000000000000000000..2a5a976c7b6d8e921b0b4653052cd375d35a4d18
--- /dev/null
+++ b/beat/core/test/prefix/databases/large/2.py
@@ -0,0 +1,87 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+
+###################################################################################
+#                                                                                 #
+# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/               #
+# Contact: beat.support@idiap.ch                                                  #
+#                                                                                 #
+# Redistribution and use in source and binary forms, with or without              #
+# modification, are permitted provided that the following conditions are met:     #
+#                                                                                 #
+# 1. Redistributions of source code must retain the above copyright notice, this  #
+# list of conditions and the following disclaimer.                                #
+#                                                                                 #
+# 2. Redistributions in binary form must reproduce the above copyright notice,    #
+# this list of conditions and the following disclaimer in the documentation       #
+# and/or other materials provided with the distribution.                          #
+#                                                                                 #
+# 3. Neither the name of the copyright holder nor the names of its contributors   #
+# may be used to endorse or promote products derived from this software without   #
+# specific prior written permission.                                              #
+#                                                                                 #
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND #
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED   #
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE          #
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE    #
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL      #
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR      #
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER      #
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,   #
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE   #
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.            #
+#                                                                                 #
+###################################################################################
+
+
+import numpy
+from collections import namedtuple
+from beat.backend.python.database import View
+
+
+# ----------------------------------------------------------
+
+
+class LargeView(View):
+    def __init__(self):
+        super(LargeView, self).__init__()
+        numpy.random.seed(0)  # So it is kept reproducible
+
+    def index(self, root_folder, parameters):
+        Entry = namedtuple("Entry", ["out"])
+
+        entries = []
+        for i in range(0, 1000):
+            entries.append(Entry(numpy.int32(numpy.random.randint(100, size=(1000,)))))
+
+        return entries
+
+    def get(self, output, index):
+        obj = self.objs[index]
+
+        if output == "out":
+            return {"value": obj.out}
+
+
+# ----------------------------------------------------------
+
+
+class SmallView(View):
+    def __init__(self):
+        super(SmallView, self).__init__()
+        numpy.random.seed(0)  # So it is kept reproducible
+
+    def index(self, root_folder, parameters):
+        Entry = namedtuple("Entry", ["out"])
+
+        entries = []
+        for i in range(0, 1000):
+            entries.append(Entry(numpy.int32(numpy.random.randint(0, 100))))
+
+        return entries
+
+    def get(self, output, index):
+        obj = self.objs[index]
+
+        if output == "out":
+            return {"value": obj.out}
diff --git a/beat/core/test/prefix/databases/simple/2.json b/beat/core/test/prefix/databases/simple/2.json
new file mode 100644
index 0000000000000000000000000000000000000000..f871a13fa326439abd5dc31699b4fa3d3ac2ad34
--- /dev/null
+++ b/beat/core/test/prefix/databases/simple/2.json
@@ -0,0 +1,30 @@
+{
+    "root_folder": "/tmp/foo/bar",
+    "protocols": [
+        {
+            "name": "protocol",
+            "template": "protocol/1",
+            "views": {
+                "set": {
+                    "view": "View"
+                },
+                "set2": {
+                    "view": "View2"
+                }
+            }
+        },
+        {
+            "name": "protocol2",
+            "template": "protocol2/1",
+            "views": {
+                "set": {
+                    "view": "LargeView"
+                },
+                "set2": {
+                    "view": "View2"
+                }
+            }
+        }
+    ],
+    "schema_version": 2
+}
diff --git a/beat/core/test/prefix/databases/simple/2.py b/beat/core/test/prefix/databases/simple/2.py
new file mode 100644
index 0000000000000000000000000000000000000000..e306693b0e8cc617d187cd0143a1c8d6e62559cb
--- /dev/null
+++ b/beat/core/test/prefix/databases/simple/2.py
@@ -0,0 +1,22 @@
+class View:
+    def setup(
+        self,
+        root_folder,
+        outputs,
+        parameters,
+        force_start_index=None,
+        force_end_index=None,
+    ):
+        """Initializes the database"""
+
+        return True
+
+    def done(self):
+        """Should return ``True``, when data is finished"""
+
+        return True
+
+    def next(self):
+        """Loads the next data block on ``outputs``"""
+
+        return True
diff --git a/beat/core/test/prefix/protocoltemplates/large/1.json b/beat/core/test/prefix/protocoltemplates/large/1.json
new file mode 100644
index 0000000000000000000000000000000000000000..6e5978960ee7eee946ea9eb68d7cccf4ad1ed279
--- /dev/null
+++ b/beat/core/test/prefix/protocoltemplates/large/1.json
@@ -0,0 +1,11 @@
+{
+    "schema_version": 1,
+    "sets": [
+        {
+            "name": "data",
+            "outputs": {
+                "out": "user/empty_1d_array_of_integers/1"
+            }
+        }
+    ]
+}
diff --git a/beat/core/test/prefix/protocoltemplates/protocol/1.json b/beat/core/test/prefix/protocoltemplates/protocol/1.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ec082f303681488db3a7c31725e24bcc90e6df2
--- /dev/null
+++ b/beat/core/test/prefix/protocoltemplates/protocol/1.json
@@ -0,0 +1,17 @@
+{
+    "schema_version": 1,
+    "sets": [
+        {
+            "name": "set",
+            "outputs": {
+                "out": "user/single_integer/1"
+            }
+        },
+        {
+            "name": "set2",
+            "outputs": {
+                "out": "user/single_integer/1"
+            }
+        }
+    ]
+}
diff --git a/beat/core/test/prefix/protocoltemplates/protocol2/1.json b/beat/core/test/prefix/protocoltemplates/protocol2/1.json
new file mode 100644
index 0000000000000000000000000000000000000000..1ec082f303681488db3a7c31725e24bcc90e6df2
--- /dev/null
+++ b/beat/core/test/prefix/protocoltemplates/protocol2/1.json
@@ -0,0 +1,17 @@
+{
+    "schema_version": 1,
+    "sets": [
+        {
+            "name": "set",
+            "outputs": {
+                "out": "user/single_integer/1"
+            }
+        },
+        {
+            "name": "set2",
+            "outputs": {
+                "out": "user/single_integer/1"
+            }
+        }
+    ]
+}
diff --git a/beat/core/test/prefix/protocoltemplates/small/1.json b/beat/core/test/prefix/protocoltemplates/small/1.json
new file mode 100644
index 0000000000000000000000000000000000000000..b14901ed5cf2bb9fe324c27cb3fbf6bc1848073b
--- /dev/null
+++ b/beat/core/test/prefix/protocoltemplates/small/1.json
@@ -0,0 +1,11 @@
+{
+    "schema_version": 1,
+    "sets": [
+        {
+            "name": "data",
+            "outputs": {
+                "out": "user/single_integer/1"
+            }
+        }
+    ]
+}
diff --git a/beat/core/test/test_bcp.py b/beat/core/test/test_bcp.py
index 5fe5702e0749cf81f99715d2bb85e0711d71ccf2..10d398d0ef04ffbb141cf290ba1992fc10743457 100644
--- a/beat/core/test/test_bcp.py
+++ b/beat/core/test/test_bcp.py
@@ -322,7 +322,7 @@ class TestBCPDocker(TestBCP):
 
     @classmethod
     def setUpClass(cls):
-        cls.images_cache = os.path.join(tmp_prefix, "docker_images_cache.json")
+        cls.docker_images_cache = os.path.join(tmp_prefix, "docker_images_cache.json")
         cls.host = Host(images_cache=cls.docker_images_cache, raise_on_errors=False)
 
 
diff --git a/beat/core/test/test_database.py b/beat/core/test/test_database.py
index 27cd3766f374719493a82db4b7fcbfdd03b9848e..8296534f611b1d890fa149bfa65a67cfc7100ffc 100644
--- a/beat/core/test/test_database.py
+++ b/beat/core/test/test_database.py
@@ -41,15 +41,21 @@ from . import prefix, tmp_prefix
 from .utils import cleanup
 
 
-@nose.tools.with_setup(teardown=cleanup)
 def test_export():
+    for i in range(1, 3):
+        yield export, f"integers_db/{i}"
+        yield export, f"simple/{i}"
+        yield export, f"large/{i}"
+
+
+@nose.tools.with_setup(teardown=cleanup)
+def export(db_name):
 
-    name = "integers_db/1"
-    obj = Database(prefix, name)
+    obj = Database(prefix, db_name)
     nose.tools.assert_true(obj.valid, "\n  * %s" % "\n  * ".join(obj.errors))
 
     obj.export(tmp_prefix)
 
     # load from tmp_prefix and validates
-    exported = Database(tmp_prefix, name)
+    exported = Database(tmp_prefix, db_name)
     nose.tools.assert_true(exported.valid, "\n  * %s" % "\n  * ".join(exported.errors))
diff --git a/beat/core/test/test_protocoltemplate.py b/beat/core/test/test_protocoltemplate.py
new file mode 100644
index 0000000000000000000000000000000000000000..526a85602e81f87e821c765d666bda27b9686684
--- /dev/null
+++ b/beat/core/test/test_protocoltemplate.py
@@ -0,0 +1,65 @@
+#!/usr/bin/env python
+# vim: set fileencoding=utf-8 :
+
+###################################################################################
+#                                                                                 #
+# Copyright (c) 2019 Idiap Research Institute, http://www.idiap.ch/               #
+# Contact: beat.support@idiap.ch                                                  #
+#                                                                                 #
+# Redistribution and use in source and binary forms, with or without              #
+# modification, are permitted provided that the following conditions are met:     #
+#                                                                                 #
+# 1. Redistributions of source code must retain the above copyright notice, this  #
+# list of conditions and the following disclaimer.                                #
+#                                                                                 #
+# 2. Redistributions in binary form must reproduce the above copyright notice,    #
+# this list of conditions and the following disclaimer in the documentation       #
+# and/or other materials provided with the distribution.                          #
+#                                                                                 #
+# 3. Neither the name of the copyright holder nor the names of its contributors   #
+# may be used to endorse or promote products derived from this software without   #
+# specific prior written permission.                                              #
+#                                                                                 #
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND #
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED   #
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE          #
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE    #
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL      #
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR      #
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER      #
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,   #
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE   #
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.            #
+#                                                                                 #
+###################################################################################
+
+
+import nose.tools
+from ..protocoltemplate import ProtocolTemplate
+
+from . import prefix, tmp_prefix
+from .utils import cleanup
+
+
+def test_export():
+    for protocol_name in [
+        "double",
+        "triple",
+        "two_sets",
+        "labelled",
+        "different_frequencies",
+    ]:
+        yield export, f"{protocol_name}/1"
+
+
+@nose.tools.with_setup(teardown=cleanup)
+def export(protocol_name):
+
+    obj = ProtocolTemplate(prefix, protocol_name)
+    nose.tools.assert_true(obj.valid, "\n  * %s" % "\n  * ".join(obj.errors))
+
+    obj.export(tmp_prefix)
+
+    # load from tmp_prefix and validates
+    exported = ProtocolTemplate(tmp_prefix, protocol_name)
+    nose.tools.assert_true(exported.valid, "\n  * %s" % "\n  * ".join(exported.errors))
diff --git a/beat/core/test/test_schema.py b/beat/core/test/test_schema.py
index 8f76da7fa56eb55ba5dc84fcc78dc75be58d7ebe..5940fdbc95e119fd9b32ef79a38bd1ba88addff8 100644
--- a/beat/core/test/test_schema.py
+++ b/beat/core/test/test_schema.py
@@ -37,31 +37,14 @@
 from ..schema import load_schema
 
 
-def test_common():
-
-    load_schema("common")
-
-
-def test_dataformat():
-
-    load_schema("dataformat")
-
-
-def test_algorithm():
-
-    load_schema("algorithm")
-
-
-def test_database():
-
-    load_schema("database")
-
-
-def test_toolchain():
-
-    load_schema("toolchain")
-
-
-def test_experiment():
-
-    load_schema("experiment")
+def test_load_schema():
+    for item in [
+        "common",
+        "dataformat",
+        "algorithm",
+        "protocoltemplate",
+        "database",
+        "toolchain",
+        "experiment",
+    ]:
+        yield load_schema, item
diff --git a/beat/core/test/test_worker.py b/beat/core/test/test_worker.py
index 2d8196a1cae0b4de6654810138fb61b3ac4f918d..6249c4707d0edfa4a082ade6f6d253e22043686c 100644
--- a/beat/core/test/test_worker.py
+++ b/beat/core/test/test_worker.py
@@ -46,6 +46,9 @@ import queue
 from time import time
 from time import sleep
 
+from ddt import ddt
+from ddt import idata
+
 from ..scripts import worker
 from ..worker import WorkerController
 from ..database import Database
@@ -66,6 +69,8 @@ PORT = find_free_port()
 
 # ----------------------------------------------------------
 
+DATABASES = [f"integers_db/{i}" for i in range(1, 3)]
+
 
 CONFIGURATION1 = {
     "queue": "queue",
@@ -133,6 +138,19 @@ CONFIGURATION2 = {
 # ----------------------------------------------------------
 
 
+def prepare_database(db_name):
+    CONFIGURATION1["inputs"]["in"]["database"] = db_name
+    CONFIGURATION2["inputs"]["in"]["database"] = db_name
+
+    for _, input_cfg in CONFIGURATION1["inputs"].items():
+        database = Database(prefix, input_cfg["database"])
+        view = database.view(input_cfg["protocol"], input_cfg["set"])
+        view.index(os.path.join(tmp_prefix, input_cfg["path"]))
+
+
+# ----------------------------------------------------------
+
+
 class ControllerProcess(multiprocessing.Process):
     def __init__(self, queue):
         super(ControllerProcess, self).__init__()
@@ -271,12 +289,6 @@ class TestWorkerBase(unittest.TestCase):
 
         self.assertTrue(name not in self.controller.workers)
 
-    def prepare_databases(self, configuration):
-        for _, input_cfg in configuration["inputs"].items():
-            database = Database(prefix, input_cfg["database"])
-            view = database.view(input_cfg["protocol"], input_cfg["set"])
-            view.index(os.path.join(tmp_prefix, input_cfg["path"]))
-
 
 # ----------------------------------------------------------
 
@@ -361,6 +373,7 @@ class TestConnection(TestWorkerBase):
 # ----------------------------------------------------------
 
 
+@ddt
 class TestOneWorker(TestWorkerBase):
     def setUp(self):
         super(TestOneWorker, self).setUp()
@@ -370,8 +383,6 @@ class TestOneWorker(TestWorkerBase):
 
         self.wait_for_worker_connection(WORKER1)
 
-        self.prepare_databases(CONFIGURATION1)
-
     def _wait(self, max=200):
         message = None
         nb = 0
@@ -398,14 +409,20 @@ class TestOneWorker(TestWorkerBase):
 
         self.assertEqual(result["status"], 0)
 
-    def test_success(self):
+    @idata(DATABASES)
+    def test_success(self, db_name):
+        prepare_database(db_name)
+
         self.controller.execute(WORKER1, 1, CONFIGURATION1)
 
         message = self._wait()
 
         self._check_done(message, WORKER1, 1)
 
-    def test_processing_error(self):
+    @idata(DATABASES)
+    def test_processing_error(self, db_name):
+        prepare_database(db_name)
+
         config = dict(CONFIGURATION1)
         config["algorithm"] = "legacy/process_crash/1"
 
@@ -424,7 +441,10 @@ class TestOneWorker(TestWorkerBase):
         self.assertEqual(result["status"], 1)
         self.assertTrue("a = b" in result["user_error"])
 
-    def test_error_unknown_algorithm(self):
+    @idata(DATABASES)
+    def test_error_unknown_algorithm(self, db_name):
+        prepare_database(db_name)
+
         config = dict(CONFIGURATION1)
         config["algorithm"] = "user/unknown/1"
 
@@ -439,7 +459,10 @@ class TestOneWorker(TestWorkerBase):
         self.assertEqual(job_id, 1)
         self.assertTrue(len(data) > 0)
 
-    def test_error_syntax_error(self):
+    @idata(DATABASES)
+    def test_error_syntax_error(self, db_name):
+        prepare_database(db_name)
+
         config = dict(CONFIGURATION1)
         config["algorithm"] = "legacy/syntax_error/1"
 
@@ -454,7 +477,10 @@ class TestOneWorker(TestWorkerBase):
         self.assertEqual(job_id, 1)
         self.assertTrue(len(data) > 0)
 
-    def test_multiple_jobs(self):
+    @idata(DATABASES)
+    def test_multiple_jobs(self, db_name):
+        prepare_database(db_name)
+
         config = dict(CONFIGURATION1)
         config["algorithm"] = "user/integers_echo_slow/1"
 
@@ -467,7 +493,10 @@ class TestOneWorker(TestWorkerBase):
         message = self._wait()
         self._check_done(message, WORKER1, 2)
 
-    def test_reuse(self):
+    @idata(DATABASES)
+    def test_reuse(self, db_name):
+        prepare_database(db_name)
+
         self.controller.execute(WORKER1, 1, CONFIGURATION1)
         message = self._wait()
         self._check_done(message, WORKER1, 1)
@@ -476,7 +505,10 @@ class TestOneWorker(TestWorkerBase):
         message = self._wait()
         self._check_done(message, WORKER1, 2)
 
-    def test_cancel(self):
+    @idata(DATABASES)
+    def test_cancel(self, db_name):
+        prepare_database(db_name)
+
         config = dict(CONFIGURATION1)
         config["algorithm"] = "user/integers_echo_slow/1"
 
@@ -508,6 +540,7 @@ class TestOneWorker(TestWorkerBase):
 # ----------------------------------------------------------
 
 
+@ddt
 class TestTwoWorkers(TestWorkerBase):
     def setUp(self):
         self.tearDown()  # In case another test failed badly during its setUp()
@@ -520,7 +553,9 @@ class TestTwoWorkers(TestWorkerBase):
         self.wait_for_worker_connection(WORKER1)
         self.wait_for_worker_connection(WORKER2)
 
-    def _test_success_one_worker(self, worker_name):
+    def _test_success_one_worker(self, worker_name, db_name):
+        prepare_database(db_name)
+
         self.controller.execute(worker_name, 1, CONFIGURATION1)
 
         message = None
@@ -538,13 +573,16 @@ class TestTwoWorkers(TestWorkerBase):
 
         self.assertEqual(result["status"], 0)
 
-    def test_success_worker1(self):
-        self._test_success_one_worker(WORKER1)
+    @idata(DATABASES)
+    def test_success_worker1(self, db_name):
+        self._test_success_one_worker(WORKER1, db_name)
 
-    def test_success_worker2(self):
-        self._test_success_one_worker(WORKER2)
+    @idata(DATABASES)
+    def test_success_worker2(self, db_name):
+        self._test_success_one_worker(WORKER2, db_name)
 
-    def test_success_both_workers(self):
+    @idata(DATABASES)
+    def test_success_both_workers(self, db_name):
         def _check(worker, status, job_id, data):
             self.assertEqual(status, WorkerController.DONE)
 
@@ -557,6 +595,8 @@ class TestTwoWorkers(TestWorkerBase):
             result = json.loads(data[0])
             self.assertEqual(result["status"], 0)
 
+        prepare_database(db_name)
+
         self.controller.execute(WORKER1, 1, CONFIGURATION1)
         self.controller.execute(WORKER2, 2, CONFIGURATION2)
 
diff --git a/beat/core/test/utils.py b/beat/core/test/utils.py
index d566048f680d841d66153f9aac59edbb8c06d29d..ab2191035141c925db00bbdba738b2f24ec65c12 100644
--- a/beat/core/test/utils.py
+++ b/beat/core/test/utils.py
@@ -48,8 +48,8 @@ import docker
 
 # Images used for docker-enabled tests within this and other BEAT packages
 DOCKER_TEST_IMAGES = {
-    "docker.idiap.ch/beat/beat.env.system.python": "1.3.0r4",
-    "docker.idiap.ch/beat/beat.env.db.examples": "1.4.0r4",
+    "docker.idiap.ch/beat/beat.env.system.python": "1.3.0r5",
+    "docker.idiap.ch/beat/beat.env.db.examples": "1.4.0r5",
     "docker.idiap.ch/beat/beat.env.cxx": "2.0.0r1",
     "docker.idiap.ch/beat/beat.env.client": "2.0.0r1",
 }
diff --git a/conda/meta.yaml b/conda/meta.yaml
index fc13b17cd6072d26c4df7947ba645253dd1ad210..b02b796e30325e1bba00d38dc3a9a079a1c87991 100644
--- a/conda/meta.yaml
+++ b/conda/meta.yaml
@@ -37,7 +37,7 @@ requirements:
     - pyzmq
     - simplejson
     - six
-    - beat.backend.python >=1.6.0a0
+    - beat.backend.python >=1.7.0b0
     - matplotlib
     - pillow
 
@@ -46,6 +46,7 @@ test:
     - bob-devel {{ bob_devel }}.*
     - beat-devel {{ beat_devel }}.*
     - bob.extension
+    - ddt
     - nose
     - coverage
     - sphinx