diff --git a/.ci/bitbucket_api_get.bash b/.ci/bitbucket_api_get.bash
new file mode 100644
index 0000000000000000000000000000000000000000..3ca93cad19def545edb12591223ec1c5daabc46e
--- /dev/null
+++ b/.ci/bitbucket_api_get.bash
@@ -0,0 +1,7 @@
+#!/bin/bash
+REPOSITORY=$1
+FILENAME_XSD=$2
+TOPIC_BRANCH=$3
+
+wget https://api.bitbucket.org/2.0/repositories/hbpneurorobotics/${REPOSITORY}/src/${TOPIC_BRANCH}/${FILENAME_XSD} -O ${FILENAME_XSD} \
+    && echo ${FILENAME_XSD}:${TOPIC_BRANCH}
diff --git a/.ci/build.bash b/.ci/build.bash
new file mode 100644
index 0000000000000000000000000000000000000000..bf962f7df9ed3f5adb302887c4b81d0672e97574
--- /dev/null
+++ b/.ci/build.bash
@@ -0,0 +1,81 @@
+#!/bin/bash
+
+set -e
+set -x
+
+whoami
+env | sort
+pwd
+
+# import environment
+if [ -f .ci/env ]; then
+    # add quotes to all vars (but do it once)
+    sudo sed -i -E 's/="*(.*[^"])"*$/="\1"/' .ci/env 
+    source '.ci/env'
+fi
+
+if [ -z ${PYTHON_VERSION_MAJOR_MINOR} ]; then
+    export PYTHON_VERSION_MAJOR=$(python -c "import sys; print(sys.version_info.major)")
+    export PYTHON_VERSION_MAJOR_MINOR=$(python -c "import sys; print('{}.{}'.format(sys.version_info.major, sys.version_info.minor))")
+fi
+
+export HBP=$WORKSPACE
+cd $WORKSPACE
+export PYTHONPATH=
+source ${USER_SCRIPTS_DIR}/nrp_variables
+# source ${USER_SCRIPTS_DIR}/nrp_aliases
+cd ${BRAIN_SIMULATION_DIR}
+
+
+# Configure build has to be placed before make devinstall
+export VIRTUAL_ENV_PATH=$VIRTUAL_ENV
+export NRP_INSTALL_MODE=dev
+export export PYTHONPATH=$VIRTUAL_ENV_PATH/lib/python${PYTHON_VERSION_MAJOR_MINOR}/site-packages:$PYTHONPATH
+# Concatenate all build requirements, ensure newline in between
+(echo; cat ${HBP}/${EXP_CONTROL_DIR}/hbp_nrp_excontrol/requirements.txt) >> hbp_nrp_distributed_nest/requirements.txt
+(echo; cat ${HBP}/${CLE_DIR}/hbp_nrp_cle/requirements.txt) >> hbp_nrp_distributed_nest/requirements.txt
+(echo; cat ${HBP}/${EXDBACKEND_DIR}/hbp_nrp_commons/requirements.txt) >> hbp_nrp_distributed_nest/requirements.txt
+
+# Checkout config.ini.sample from user-scripts
+cp ${HBP}/${USER_SCRIPTS_DIR}/config_files/CLE/config.ini.sample ${HBP}/${CLE_DIR}/hbp_nrp_cle/hbp_nrp_cle/config.ini
+
+# Obtain schemas
+REPOSITORY=experiments
+[ -z "$(git ls-remote --heads  git@bitbucket.org:hbpneurorobotics/${REPOSITORY}.git ${TOPIC_BRANCH})" ] \
+        && CO_BRANCH="${DEFAULT_BRANCH}" \
+        || CO_BRANCH="${TOPIC_BRANCH}"
+bash ./.ci/bitbucket_api_get.bash "${REPOSITORY}" bibi_configuration.xsd "${CO_BRANCH}" 
+bash ./.ci/bitbucket_api_get.bash "${REPOSITORY}" ExDConfFile.xsd "${CO_BRANCH}"
+python .ci/ci_download_directory.py https://bitbucket.org/hbpneurorobotics/experiments/src/development/hbp-scxml/
+REPOSITORY=models
+[ -z "$(git ls-remote --heads  git@bitbucket.org:hbpneurorobotics/${REPOSITORY}.git ${TOPIC_BRANCH})" ] \
+        && CO_BRANCH="${DEFAULT_BRANCH}" \
+        || CO_BRANCH="${TOPIC_BRANCH}"
+bash ./.ci/bitbucket_api_get.bash "${REPOSITORY}" robot_model_configuration.xsd "${CO_BRANCH}"
+bash ./.ci/bitbucket_api_get.bash "${REPOSITORY}" environment_model_configuration.xsd "${CO_BRANCH}"
+
+# Generate schemas
+pushd $VIRTUAL_ENV_PATH/lib/python${PYTHON_VERSION_MAJOR_MINOR}/site-packages && rm -f hbp-nrp-distributed-nest.egg-link && popd
+make devinstall # Otherwise it can't find pyxbgen
+export pyxb_version=`grep "pyxb" ${HBP}/${EXDBACKEND_DIR}/hbp_nrp_commons/requirements.txt`
+. $VIRTUAL_ENV_PATH/bin/activate \
+        && pyxbgen -u bibi_configuration.xsd -m bibi_api_gen \
+        && pyxbgen -u ExDConfFile.xsd -m exp_conf_api_gen \
+        && pyxbgen -u robot_model_configuration.xsd -m robot_conf_api_gen \
+        && pyxbgen -u environment_model_configuration.xsd -m environment_conf_api_gen
+mv bibi_api_gen.py exp_conf_api_gen.py _sc.py robot_conf_api_gen.py environment_conf_api_gen.py ${HBP}/${EXDBACKEND_DIR}/hbp_nrp_commons/hbp_nrp_commons/generated
+touch ${HBP}/${EXDBACKEND_DIR}/hbp_nrp_commons/hbp_nrp_commons/generated/__init__.py
+deactivate
+
+
+# Run tests
+export IGNORE_LINT='platform_venv|migrations|nest|ci_download_directory'
+# Egg-links have to be removed because make devinstall set them up wrongly
+pushd $VIRTUAL_ENV_PATH/lib/python${PYTHON_VERSION_MAJOR_MINOR}/site-packages \
+        && rm -f hbp-nrp-distributed-nest.egg-link && popd
+. $VIRTUAL_ENV_PATH/bin/activate \
+        && source /opt/ros/noetic/setup.bash \
+        && source $HBP/GazeboRosPackages/devel/setup.bash \
+        && echo "PYTHONPATH $PYTHONPATH" \
+        && make verify_base -i
+        
\ No newline at end of file
diff --git a/.ci/ci_download_directory.py b/.ci/ci_download_directory.py
new file mode 100644
index 0000000000000000000000000000000000000000..4c3b0a8d386bba275f3bac68d2acd2d5124cb067
--- /dev/null
+++ b/.ci/ci_download_directory.py
@@ -0,0 +1,60 @@
+#!/usr/bin/env python
+import os
+import sys
+from urllib.parse import urlparse
+import urllib.request
+
+try:
+    import json
+except:
+    import simplejson as json
+
+
+def open_directory(API_PATH, username, repo, slug, path):
+    directory_url = "%s/%s/%s/src/%s/%s" % (API_PATH, username, repo, slug, path)
+    print(directory_url)
+    json_data_url_handle = urllib.request.urlopen(directory_url)
+
+    if json_data_url_handle.code != 200:
+        print("url %s not found" % directory_url)
+        exit()
+
+    json_directory = json.loads(json_data_url_handle.read())
+    page_exists = True
+
+    while page_exists:
+        for item in json_directory["values"]:
+            print(json.dumps(item, indent=4))
+            if item["type"] == "commit_directory":
+                open_directory(API_PATH, username, repo, slug, item["path"])
+
+        for file in json_directory["values"]:
+            if file["type"] == "commit_file" and file["mimetype"] == "text/xml":
+                try:
+                    os.makedirs(os.path.dirname(file["path"]))
+                except OSError:
+                    None
+                print("downloading %s" % file["path"])
+                print(file['links']['self']['href'])
+                urllib.request.urlretrieve(file['links']['self']['href'], file['path'])
+        if "next" in json_directory:
+            json_data_url_handle = urllib.request.urlopen(json_directory['next'])
+            if json_data_url_handle.code != 200:
+                print("url %s not found" % directory_url)
+                exit()
+            json_directory = json.loads(json_data_url_handle.read())
+        else:
+            page_exists = False
+
+if (
+    len(sys.argv) != 2 or
+    sys.argv[1].find("https://bitbucket.org/") != 0 or
+    sys.argv[1].find("/src/") == -1
+):
+    print("usage: python download_directory.py.py https://bitbucket.org/ws/repo/src/branch/demo/")
+    exit()
+
+API_PATH = "https://api.bitbucket.org/2.0/repositories"
+null, username, repo, null, slug, path = urlparse(sys.argv[1]).path.split("/", 5)
+print(username, repo, slug, path)
+open_directory(API_PATH, username, repo, slug, path)
diff --git a/Jenkinsfile b/Jenkinsfile
new file mode 100644
index 0000000000000000000000000000000000000000..059c0021fc9db9c5815db146687472f1320409fc
--- /dev/null
+++ b/Jenkinsfile
@@ -0,0 +1,144 @@
+#!groovy
+// Load shared library at master branch
+// the path to the repo with this library should be specified in Jenkins
+// https://tomd.xyz/jenkins-shared-library/
+// https://www.jenkins.io/doc/book/pipeline/shared-libraries/
+@Library('nrp-shared-libs@master') _
+
+pipeline {
+    environment {
+        USER_SCRIPTS_DIR = "user-scripts"
+        ADMIN_SCRIPTS_DIR = "admin-scripts"
+        GAZEBO_ROS_DIR = "GazeboRosPackages"
+        EXP_CONTROL_DIR = "ExperimentControl"
+        CLE_DIR = "CLE"
+        BRAIN_SIMULATION_DIR = "BrainSimulation"
+        EXDBACKEND_DIR = "ExDBackend"
+        // GIT_CHECKOUT_DIR is a dir of the main project (that was pushed)
+        GIT_CHECKOUT_DIR = "${env.BRAIN_SIMULATION_DIR}"
+
+        // selectTopicBranch function is used to choose the correct branch name as topic
+        // the function is defined in shared libs
+        // 
+        // In case there is a PR for a branch, then Jenkins runs a pipeline for this pull request, not for the branch, 
+        // even if there are new commits to the branch (until PR is not closed). The BRANCH_NAME variable in this case is something like PR-###
+        // The name of the branch which is merged is stored in CHANGE_BRANCH variable. Thus, we should choose CHANGE_BRANCH as topic
+        //
+        // If there is a branch without PR, then Jenkins creates build for it normally for every push and the branch name is stored in BRANCH_NAME variable.
+        // CHANGE_BRANCH is empty in this case. Thus, we choose BRANCH_NAME as topic for branches without PR.
+        TOPIC_BRANCH = selectTopicBranch(env.BRANCH_NAME, env.CHANGE_BRANCH)
+        DEFAULT_BRANCH = 'development'
+
+        NRP_COVERAGE_BRANCH=0
+        NRP_COVERAGE_LINE=23
+    }
+    agent {
+        docker {
+            // NEXUS_REGISTRY_IP and NEXUS_REGISTRY_PORT are Jenkins global variables
+            image "${env.NEXUS_REGISTRY_IP}:${env.NEXUS_REGISTRY_PORT}/nrp:development"
+            args '--entrypoint="" -u root --privileged'
+        }
+    }
+    options { 
+        // Skip code checkout prior running pipeline (only Jenkinsfile is checked out)
+        skipDefaultCheckout true
+    }
+
+    stages {
+        stage('Code checkout') {
+            steps {
+                // clear workspace
+                sh "rm -rf *"
+                // Notify BitBucket on the start of the job
+                // The Bitbucket Build Status Notifier is used
+                // REF: https://plugins.jenkins.io/bitbucket-build-status-notifier/
+                
+                bitbucketStatusNotify(buildState: 'INPROGRESS', buildName: 'Code checkout')
+
+                // Debug information on available environment
+                echo sh(script: 'env|sort', returnStdout: true)
+
+                // Checkout main project to GIT_CHECKOUT_DIR
+                dir(env.GIT_CHECKOUT_DIR) {
+                    checkout scm
+                    sh 'chown -R "${USER}" ./'
+                }
+
+                // Clone all dependencies
+                // cloneRepoTopic: 
+                //      1 - directory to checkout
+                //      2 - repo
+                //      3 - name of topic branch
+                //      4 - default branch if topic unavailable
+                //      5 - username for chown
+                
+                // TODO: fix default branch to master when admin-scripts CI is merged
+                cloneRepoTopic(env.ADMIN_SCRIPTS_DIR,   'git@bitbucket.org:hbpneurorobotics/admin-scripts.git',     env.TOPIC_BRANCH, 'master',           '${USER}')
+                cloneRepoTopic(env.USER_SCRIPTS_DIR,    'git@bitbucket.org:hbpneurorobotics/user-scripts.git',      env.TOPIC_BRANCH, env.DEFAULT_BRANCH, '${USER}') 
+                cloneRepoTopic(env.GAZEBO_ROS_DIR,      'git@bitbucket.org:hbpneurorobotics/gazeborospackages.git', env.TOPIC_BRANCH, env.DEFAULT_BRANCH, '${USER}') 
+                cloneRepoTopic(env.EXP_CONTROL_DIR,     'git@bitbucket.org:hbpneurorobotics/experimentcontrol.git', env.TOPIC_BRANCH, env.DEFAULT_BRANCH, '${USER}') 
+                cloneRepoTopic(env.CLE_DIR,             'git@bitbucket.org:hbpneurorobotics/cle.git',               env.TOPIC_BRANCH, env.DEFAULT_BRANCH, '${USER}') 
+                cloneRepoTopic(env.EXDBACKEND_DIR,      'git@bitbucket.org:hbpneurorobotics/exdbackend.git',        env.TOPIC_BRANCH, env.DEFAULT_BRANCH, '${USER}') 
+                
+            }
+        }
+
+        stage('Build GazeboRosPackages') {
+            steps {
+                bitbucketStatusNotify(buildState: 'INPROGRESS', buildName: 'Building GazeboRosPackages')
+                
+                // Use GazeboRosPackages build script
+                dir(env.GAZEBO_ROS_DIR){
+                    // Determine explicitly the shell as bash (needed for proper user-scripts operation)
+                    sh 'bash .ci/build.bash ${WORKSPACE}/${USER_SCRIPTS_DIR}/'
+                }
+                
+            }
+        }
+        stage('Build and test BrainSimulation') {
+            steps {
+                bitbucketStatusNotify(buildState: 'INPROGRESS', buildName: 'Build and test ' + env.GIT_CHECKOUT_DIR)
+                dir(env.GIT_CHECKOUT_DIR){
+                    // this is a workaround to pass all env vars into script run by the other user (now we are root)
+                    // STAGE_NAME and CHANGE_* have spaces and is not imported then normally, thus delete it, because it's unneeded
+                    // enquote all vars to avoid problems with spaces
+                    sh 'env > .ci/env'
+                    sh 'sudo -H -u bbpnrsoa bash .ci/build.bash'
+
+                    // deliver artifacts
+                    step([$class: 'CoberturaPublisher', 
+                        autoUpdateHealth: true, 
+                        autoUpdateStability: false, 
+                        coberturaReportFile: 'coverage.xml', 
+                        failUnhealthy: true, 
+                        failUnstable: false, 
+                        maxNumberOfBuilds: 0, 
+                        onlyStable: false, 
+                        sourceEncoding: 'ASCII', 
+                        zoomCoverageChart: false,
+                        lineCoverageTargets: "90.0, 0, 0"])
+                    archiveArtifacts 'coverage.xml'
+                }
+            }
+        }
+    }
+
+    post {
+        always {
+            dir(env.GIT_CHECKOUT_DIR){
+                archiveArtifacts 'p*.*'
+                archiveArtifacts 'test-reports/*.*'
+                junit 'test-reports/*.xml'
+            }
+        }
+        aborted {
+            bitbucketStatusNotify(buildState: 'FAILED', buildDescription: 'Build aborted!')
+        }
+        failure {
+            bitbucketStatusNotify(buildState: 'FAILED', buildDescription: 'Build failed, see console output!')
+        }
+        success{
+            bitbucketStatusNotify(buildState: 'SUCCESSFUL', buildDescription: 'branch ' + env.GIT_BRANCH)
+        }
+    }
+}