diff --git a/CHANGELOG b/CHANGELOG
index 78678e8a57f3d3ac835b7ee1db5b13313db117a6..619737659da8ef4cd3c21a798cbbf784ec44b50d 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -2,6 +2,7 @@ minerva (19.0.0~alpha.0) stable; urgency=medium
   * Backward incompatible: copying annotations into complex children moved
     to a dedicated annotator (#2216)
   * Backward incompatible: caching should be done in the background (#1656)
+  * Backward incompatible: annotations are performed asynchronously (#1657)
   * Backward incompatible: MiRNA search has been removed (#2102)
   * Backward incompatible: degraded does not have a name (#2116)
   * Backward incompatible: custom annotation validation on project upload
diff --git a/annotation/src/main/java/lcsb/mapviewer/annotation/services/ModelAnnotator.java b/annotation/src/main/java/lcsb/mapviewer/annotation/services/ModelAnnotator.java
index 0861b2a4170de7ef671a11d5dbca878372226d7e..a00cad55195a4d25b71f710aa45f4e32acf0c084 100644
--- a/annotation/src/main/java/lcsb/mapviewer/annotation/services/ModelAnnotator.java
+++ b/annotation/src/main/java/lcsb/mapviewer/annotation/services/ModelAnnotator.java
@@ -4,15 +4,21 @@ import lcsb.mapviewer.annotation.services.annotators.AnnotatorException;
 import lcsb.mapviewer.annotation.services.annotators.ElementAnnotator;
 import lcsb.mapviewer.annotation.services.annotators.IElementAnnotator;
 import lcsb.mapviewer.common.IProgressUpdater;
-import lcsb.mapviewer.common.exception.InvalidArgumentException;
+import lcsb.mapviewer.model.Project;
 import lcsb.mapviewer.model.map.BioEntity;
 import lcsb.mapviewer.model.map.MiriamType;
-import lcsb.mapviewer.model.map.model.Model;
+import lcsb.mapviewer.model.map.reaction.Reaction;
+import lcsb.mapviewer.model.map.species.Element;
 import lcsb.mapviewer.model.user.UserAnnotationSchema;
 import lcsb.mapviewer.model.user.UserClassAnnotators;
 import lcsb.mapviewer.model.user.annotator.AnnotatorData;
 import lcsb.mapviewer.modelutils.map.ClassTreeNode;
 import lcsb.mapviewer.modelutils.map.ElementUtils;
+import lcsb.mapviewer.persist.DbUtils;
+import lcsb.mapviewer.persist.dao.map.ReactionDao;
+import lcsb.mapviewer.persist.dao.map.ReactionProperty;
+import lcsb.mapviewer.persist.dao.map.species.ElementDao;
+import lcsb.mapviewer.persist.dao.map.species.ElementProperty;
 import lcsb.mapviewer.persist.dao.user.UserAnnotationSchemaDao;
 import org.apache.commons.collections4.ListUtils;
 import org.apache.logging.log4j.LogManager;
@@ -21,9 +27,9 @@ import org.springframework.aop.framework.Advised;
 import org.springframework.aop.support.AopUtils;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
 
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.LinkedList;
@@ -31,19 +37,11 @@ import java.util.List;
 import java.util.Map;
 import java.util.Queue;
 import java.util.Set;
+import java.util.stream.Collectors;
 
-/**
- * This class annotates all elements in the model.
- *
- * @author Piotr Gawron
- */
 @Service
-@Transactional
 public class ModelAnnotator {
 
-  /**
-   * Default class logger.
-   */
   private static final Logger logger = LogManager.getLogger();
 
   /**
@@ -51,37 +49,22 @@ public class ModelAnnotator {
    */
   private final List<IElementAnnotator> availableAnnotators;
 
-  /**
-   * List of all available {@link ElementAnnotator} objects.
-   */
-  private final List<IElementAnnotator> defaultAnnotators;
-
   private final UserAnnotationSchemaDao userAnnotationSchemaDao;
+  private final ElementDao elementDao;
+  private final ReactionDao reactionDao;
+  private final DbUtils dbUtils;
 
-  /**
-   * Constructor
-   */
   @Autowired
   public ModelAnnotator(final List<IElementAnnotator> availableAnnotators,
-                        final UserAnnotationSchemaDao userAnnotationSchemaDao) {
+                        final UserAnnotationSchemaDao userAnnotationSchemaDao,
+                        final ElementDao elementDao,
+                        final ReactionDao reactionDao,
+                        final DbUtils dbUtils) {
     this.availableAnnotators = availableAnnotators;
     this.userAnnotationSchemaDao = userAnnotationSchemaDao;
-    defaultAnnotators = new ArrayList<>();
-    for (final IElementAnnotator annotator : availableAnnotators) {
-      if (annotator.isDefault()) {
-        defaultAnnotators.add(annotator);
-      }
-    }
-  }
-
-  /**
-   * Performs all possible and automatic annotations on the model.
-   *
-   * @param model           model to update
-   * @param progressUpdater callback function used for updating progress of the function
-   */
-  public void performAnnotations(final Model model, final IProgressUpdater progressUpdater) {
-    performAnnotations(model, progressUpdater, new UserAnnotationSchema());
+    this.dbUtils = dbUtils;
+    this.elementDao = elementDao;
+    this.reactionDao = reactionDao;
   }
 
   /**
@@ -89,39 +72,69 @@ public class ModelAnnotator {
    *
    * @param inputAnnotationSchema information about {@link ElementAnnotator} objects that should be
    *                              used for a given classes
-   * @param model                 model to update
+   * @param project               project to update
    * @param progressUpdater       callback function used for updating progress of the function
    */
-  public void performAnnotations(final Model model, final IProgressUpdater progressUpdater, final UserAnnotationSchema inputAnnotationSchema) {
-
+  public void performAnnotations(final Project project, final IProgressUpdater progressUpdater, final UserAnnotationSchema inputAnnotationSchema) {
     UserAnnotationSchema annotationSchema = inputAnnotationSchema;
     if (annotationSchema != null && annotationSchema.getId() > 0) {
       annotationSchema = userAnnotationSchemaDao.getById(annotationSchema.getId());
     }
+    if (annotationSchema == null) {
+      logger.error("Annotation Schema does not exist");
+      annotationSchema = new UserAnnotationSchema();
+    }
 
+    ElementUtils elementUtils = new ElementUtils();
     progressUpdater.setProgress(0);
-    List<Model> models = new ArrayList<>();
-    models.add(model);
-    models.addAll(model.getSubmodels());
-    final double size = models.size();
+
     double counter = 0;
-    for (final Model m : models) {
-      final double ratio = counter / size;
+    Map<ElementProperty, Object> elementFilter = new HashMap<>();
+    elementFilter.put(ElementProperty.PROJECT_ID, Collections.singletonList(project.getProjectId()));
 
-      annotateModel(m, progress -> progressUpdater.setProgress(ratio * IProgressUpdater.MAX_PROGRESS / size), annotationSchema);
-      counter++;
-    }
-  }
+    List<Integer> elementIds = dbUtils.callInTransactionWithResult(() -> elementDao.getAll(elementFilter))
+        .stream()
+        .map(Element::getId)
+        .collect(Collectors.toList());
 
-  protected void annotateModel(final Model model, final IProgressUpdater progressUpdater, final UserAnnotationSchema annotationSchema) {
-    progressUpdater.setProgress(0);
-    ElementUtils elementUtils = new ElementUtils();
+    Map<ReactionProperty, Object> reactionFilter = new HashMap<>();
+    reactionFilter.put(ReactionProperty.PROJECT_ID, Collections.singletonList(project.getProjectId()));
 
-    double counter = 0;
-    double amount = model.getElements().size() + model.getReactions().size();
+    List<Integer> reactionIds = dbUtils.callInTransactionWithResult(() -> reactionDao.getAll(reactionFilter))
+        .stream()
+        .map(Reaction::getId)
+        .collect(Collectors.toList());
+
+    double amount = elementIds.size() + reactionIds.size();
 
     // annotate all elements
-    for (final BioEntity element : model.getBioEntities()) {
+    for (final Integer reactionId : reactionIds) {
+      Reaction reaction = dbUtils.callInTransactionWithResult(() -> reactionDao.getById(reactionId));
+
+      List<AnnotatorData> list = annotationSchema.getAnnotatorsForClass(reaction.getClass());
+      if (list == null) {
+        list = getDefaultAnnotators(reaction.getClass());
+      }
+      for (final AnnotatorData annotatorData : list) {
+        IElementAnnotator elementAnnotator = null;
+        try {
+          elementAnnotator = getAnnotator(annotatorData);
+          elementAnnotator.annotateElement(reaction, annotatorData);
+          dbUtils.callInTransaction(() -> {
+            reactionDao.update(reaction);
+            return null;
+          });
+        } catch (final AnnotatorException e) {
+          logger.warn("{} annotation problem.", elementUtils.getElementTag(reaction, elementAnnotator), e);
+        }
+      }
+      counter++;
+      progressUpdater.setProgress(IProgressUpdater.MAX_PROGRESS * counter / amount);
+    }
+
+    for (final Integer elementId : elementIds) {
+      Element element = dbUtils.callInTransactionWithResult(() -> elementDao.getById(elementId));
+
       List<AnnotatorData> list = annotationSchema.getAnnotatorsForClass(element.getClass());
       if (list == null) {
         list = getDefaultAnnotators(element.getClass());
@@ -131,35 +144,30 @@ public class ModelAnnotator {
         try {
           elementAnnotator = getAnnotator(annotatorData);
           elementAnnotator.annotateElement(element, annotatorData);
+          dbUtils.callInTransaction(() -> {
+            elementDao.update(element);
+            return null;
+          });
         } catch (final AnnotatorException e) {
-          logger.warn(elementUtils.getElementTag(element, elementAnnotator) + " annotation problem.", e);
+          logger.warn("{} annotation problem.", elementUtils.getElementTag(element, elementAnnotator), e);
         }
       }
       counter++;
       progressUpdater.setProgress(IProgressUpdater.MAX_PROGRESS * counter / amount);
     }
+
   }
 
   private IElementAnnotator getAnnotator(final AnnotatorData annotatorData) throws AnnotatorException {
     for (final IElementAnnotator proxiedElementAnnotator : availableAnnotators) {
       IElementAnnotator elementAnnotator = (IElementAnnotator) unwrapProxy(proxiedElementAnnotator);
       if (annotatorData.getAnnotatorClassName().isAssignableFrom(elementAnnotator.getClass())) {
-        return elementAnnotator;
+        return proxiedElementAnnotator;
       }
     }
     throw new AnnotatorException("Annotator not supported: " + annotatorData.getAnnotatorClassName());
   }
 
-  /**
-   * Returns list of default annotators ({@link ElementAnnotator} with
-   * {@link ElementAnnotator#isDefault()} flag).
-   *
-   * @return list of default annotators
-   */
-  List<IElementAnnotator> getDefaultAnnotators() {
-    return defaultAnnotators;
-  }
-
   /**
    * Returns list of default {@link ElementAnnotator} names that are available
    * for class given in the parameter.
@@ -215,30 +223,6 @@ public class ModelAnnotator {
     return result;
   }
 
-  /**
-   * Converts list of strings into list of {@link ElementAnnotator}. Strings
-   * must be valid {@link ElementAnnotator} common names.
-   *
-   * @param list list of {@link ElementAnnotator#getCommonName()}.
-   * @return list of {@link ElementAnnotator annotators}
-   */
-  public List<IElementAnnotator> getAnnotatorsFromCommonNames(final List<AnnotatorData> list) {
-    List<IElementAnnotator> result = new ArrayList<>();
-    for (final AnnotatorData annotatorData : list) {
-      boolean added = false;
-      for (final IElementAnnotator annotator : availableAnnotators) {
-        if (annotatorData.getAnnotatorClassName().isAssignableFrom(unwrapProxy(annotator).getClass())) {
-          added = true;
-          result.add(annotator);
-        }
-      }
-      if (!added) {
-        throw new InvalidArgumentException("Unknown annotator name: " + annotatorData.getAnnotatorClassName());
-      }
-    }
-    return result;
-  }
-
   /**
    * Returns map with information about default valid {@link MiriamType miriam
    * types } for {@link BioEntity} class type.
@@ -247,15 +231,15 @@ public class ModelAnnotator {
    */
   @SuppressWarnings("unchecked")
   public Map<Class<? extends BioEntity>, Set<MiriamType>> getDefaultValidClasses() {
-    Map<Class<? extends BioEntity>, Set<MiriamType>> result = new HashMap<Class<? extends BioEntity>, Set<MiriamType>>();
+    Map<Class<? extends BioEntity>, Set<MiriamType>> result = new HashMap<>();
     ElementUtils eu = new ElementUtils();
     ClassTreeNode tree = eu.getAnnotatedElementClassTree();
 
-    Queue<ClassTreeNode> nodes = new LinkedList<ClassTreeNode>();
+    Queue<ClassTreeNode> nodes = new LinkedList<>();
     nodes.add(tree);
     while (!nodes.isEmpty()) {
       ClassTreeNode node = nodes.poll();
-      Set<MiriamType> set = new HashSet<MiriamType>();
+      Set<MiriamType> set = new HashSet<>();
       Class<? extends BioEntity> clazz = (Class<? extends BioEntity>) node.getClazz();
       for (final MiriamType mt : MiriamType.values()) {
         for (final Class<?> clazz2 : mt.getValidClass()) {
@@ -265,9 +249,7 @@ public class ModelAnnotator {
         }
       }
       result.put(clazz, set);
-      for (final ClassTreeNode child : node.getChildren()) {
-        nodes.add(child);
-      }
+      nodes.addAll(node.getChildren());
     }
     return result;
   }
@@ -302,9 +284,7 @@ public class ModelAnnotator {
         }
       }
       result.put(clazz, set);
-      for (final ClassTreeNode child : node.getChildren()) {
-        nodes.add(child);
-      }
+      nodes.addAll(node.getChildren());
     }
     return result;
   }
diff --git a/annotation/src/main/java/lcsb/mapviewer/annotation/services/annotators/CopyMissingCellDesignerMiriamDataAnnotatorImpl.java b/annotation/src/main/java/lcsb/mapviewer/annotation/services/annotators/CopyMissingCellDesignerMiriamDataAnnotatorImpl.java
index d6557cb24d6c272abcab765e5952211b35789a89..92589a43d4667ef868c136169c9d53929e4cfae6 100644
--- a/annotation/src/main/java/lcsb/mapviewer/annotation/services/annotators/CopyMissingCellDesignerMiriamDataAnnotatorImpl.java
+++ b/annotation/src/main/java/lcsb/mapviewer/annotation/services/annotators/CopyMissingCellDesignerMiriamDataAnnotatorImpl.java
@@ -14,6 +14,7 @@ import lcsb.mapviewer.persist.dao.map.species.ElementProperty;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -22,6 +23,7 @@ import java.util.List;
 import java.util.Map;
 
 @Service
+@Transactional
 public class CopyMissingCellDesignerMiriamDataAnnotatorImpl extends ElementAnnotator implements CopyMissingCellDesignerMiriamDataAnnotator {
 
   private final ElementDao elementDao;
diff --git a/annotation/src/main/java/lcsb/mapviewer/annotation/services/annotators/ElementAnnotator.java b/annotation/src/main/java/lcsb/mapviewer/annotation/services/annotators/ElementAnnotator.java
index f20fbaed811e371cf0fd3deaf862546e8cbb4048..c9725246dd9fd85d4ce8e429ac83a1ca629f898b 100644
--- a/annotation/src/main/java/lcsb/mapviewer/annotation/services/annotators/ElementAnnotator.java
+++ b/annotation/src/main/java/lcsb/mapviewer/annotation/services/annotators/ElementAnnotator.java
@@ -6,7 +6,6 @@ import lcsb.mapviewer.common.comparator.SetComparator;
 import lcsb.mapviewer.common.comparator.StringComparator;
 import lcsb.mapviewer.common.exception.InvalidArgumentException;
 import lcsb.mapviewer.common.exception.NotImplementedException;
-import lcsb.mapviewer.converter.model.celldesigner.structure.CellDesignerChemical;
 import lcsb.mapviewer.model.LogMarker;
 import lcsb.mapviewer.model.ProjectLogEntryType;
 import lcsb.mapviewer.model.map.BioEntity;
@@ -101,7 +100,7 @@ public abstract class ElementAnnotator extends CachableInterface implements IEle
    *
    * @param bioEntity  object to be annotated
    * @param parameters list of parameters passed to the annotator which is expected to be
-   *                   in the same order as its {@link this#parameterDefs}
+   *                   in the same order as its {@link this#paramsDefs}
    * @throws AnnotatorException thrown when there is a problem with annotating not related to data
    */
   @Override
@@ -109,10 +108,10 @@ public abstract class ElementAnnotator extends CachableInterface implements IEle
     if (isAnnotatable(bioEntity)) {
       final BioEntityProxy proxy = new BioEntityProxy(bioEntity, parameters);
       List<AnnotatorInputParameter> inputParameters = parameters.getInputParameters();
-      if (inputParameters.size() == 0) {
+      if (inputParameters.isEmpty()) {
         inputParameters = getAvailableInputParameters();
       }
-      if (parameters.getOutputParameters().size() == 0) {
+      if (parameters.getOutputParameters().isEmpty()) {
         parameters.addAnnotatorParameters(this.getAvailableOuputProperties());
       }
       final List<Set<Object>> inputs = getInputsParameters(bioEntity, inputParameters);
@@ -164,8 +163,7 @@ public abstract class ElementAnnotator extends CachableInterface implements IEle
    * the parameter.
    *
    * @param object object to be tested if can be annotated
-   * @return <code>true</code> if object can be annotated by this annotator,
-   * <code>false</code> otherwise
+   * @return <code>true</code> if object can be annotated by this annotator, <code>false</code> otherwise
    */
   @Override
   public boolean isAnnotatable(final BioEntity object) {
@@ -183,8 +181,7 @@ public abstract class ElementAnnotator extends CachableInterface implements IEle
    * class type.
    *
    * @param clazz class to be tested if can be annotated
-   * @return <code>true</code> if class can be annotated by this annotator,
-   * <code>false</code> otherwise
+   * @return <code>true</code> if class can be annotated by this annotator, <code>false</code> otherwise
    */
   @Override
   public boolean isAnnotatable(final Class<?> clazz) {
@@ -327,8 +324,6 @@ public abstract class ElementAnnotator extends CachableInterface implements IEle
 
   /**
    * Creates default {@link AnnotatorData} for this {@link ElementAnnotator}.
-   *
-   * @return
    */
   @Override
   public AnnotatorData createAnnotatorData() {
@@ -456,9 +451,9 @@ public abstract class ElementAnnotator extends CachableInterface implements IEle
       if (!parameters.hasOutputField(field)) {
         return false;
       }
-      if (oldCollection == null || oldCollection.size() == 0) {
+      if (oldCollection == null || oldCollection.isEmpty()) {
         return true;
-      } else if (newCollection == null || newCollection.size() == 0) {
+      } else if (newCollection == null || newCollection.isEmpty()) {
         return false;
       } else {
         final SetComparator<String> stringSetComparator = new SetComparator<>(new StringComparator());
@@ -526,13 +521,13 @@ public abstract class ElementAnnotator extends CachableInterface implements IEle
     public void setFormerSymbols(final Collection<String> formerSymbols) {
       if (originalBioEntity instanceof Element) {
         final Element element = (Element) originalBioEntity;
-        if (canAssignStringSet(formerSymbols, element.getFormerSymbols(), BioEntityField.SYMBOL)) {
+        if (canAssignStringSet(formerSymbols, element.getFormerSymbols(), BioEntityField.PREVIOUS_SYMBOLS)) {
           final List<String> sorted = new ArrayList<>(formerSymbols);
           Collections.sort(sorted);
           element.setFormerSymbols(sorted);
         }
       } else {
-        logger.warn("Cannot assign " + BioEntityField.SYMBOL.getCommonName() + " to "
+        logger.warn("Cannot assign " + BioEntityField.PREVIOUS_SYMBOLS.getCommonName() + " to "
             + originalBioEntity.getClass().getSimpleName());
       }
     }
@@ -571,7 +566,7 @@ public abstract class ElementAnnotator extends CachableInterface implements IEle
     }
 
     /**
-     * Sets {@link CellDesignerChemical#smiles}.
+     * Sets {@link Chemical#getSmiles()}.
      *
      * @param smile value to set
      */
@@ -588,7 +583,7 @@ public abstract class ElementAnnotator extends CachableInterface implements IEle
     }
 
     /**
-     * Sets {@link Species#charge}.
+     * Sets {@link Species#getCharge()}.
      *
      * @param charge value to set
      */
@@ -605,7 +600,7 @@ public abstract class ElementAnnotator extends CachableInterface implements IEle
     }
 
     /**
-     * Sets {@link Reaction#subsystem}.
+     * Sets {@link Reaction#getSubsystem()}.
      *
      * @param subsystem value to set
      */
diff --git a/annotation/src/main/java/lcsb/mapviewer/annotation/services/annotators/HgncAnnotatorImpl.java b/annotation/src/main/java/lcsb/mapviewer/annotation/services/annotators/HgncAnnotatorImpl.java
index f361ea9493f22e1a7eaf1eb2a84d6627762b8c74..a50a6452f080558ad6d76d563cf7f69d46d99959 100644
--- a/annotation/src/main/java/lcsb/mapviewer/annotation/services/annotators/HgncAnnotatorImpl.java
+++ b/annotation/src/main/java/lcsb/mapviewer/annotation/services/annotators/HgncAnnotatorImpl.java
@@ -66,7 +66,7 @@ public class HgncAnnotatorImpl extends ElementAnnotator implements HgncAnnotator
       proteinAlias.addMiriamData(getExampleValidAnnotation());
       annotateElement(proteinAlias);
 
-      if (proteinAlias.getFullName() == null || proteinAlias.getFullName().equals("")) {
+      if (proteinAlias.getFullName() == null || proteinAlias.getFullName().isEmpty()) {
         status.setStatus(ExternalServiceStatusType.CHANGED);
       } else {
         status.setStatus(ExternalServiceStatusType.OK);
@@ -98,7 +98,7 @@ public class HgncAnnotatorImpl extends ElementAnnotator implements HgncAnnotator
       Node response = XmlParser.getNode("response", xml.getChildNodes());
       Node result = XmlParser.getNode("result", response.getChildNodes());
       String count = XmlParser.getNodeAttr("numFound", result);
-      if (count == null || count.equals("") || count.equals("0")) {
+      if (count == null || count.isEmpty() || count.equals("0")) {
         // print warn info when we have only hgnc identifiers (or no
         // identifiers at all)
         logger.warn(element.getLogMarker(ProjectLogEntryType.CANNOT_FIND_INFORMATION),
@@ -124,7 +124,7 @@ public class HgncAnnotatorImpl extends ElementAnnotator implements HgncAnnotator
                 element.addMiriamData(MiriamType.ENTREZ, XmlParser.getNodeValue(node));
               } else if (type.equals("symbol")) {
                 if (!identifier.getDataType().equals(MiriamType.HGNC_SYMBOL) || !element.contains(identifier)) {
-                  // add hgnc symbol annnotation only when there was no
+                  // add hgnc symbol annotation only when there was no
                   // hgnc_symbol in the element
                   element.addMiriamData(MiriamType.HGNC_SYMBOL, XmlParser.getNodeValue(node));
                 }
@@ -214,6 +214,7 @@ public class HgncAnnotatorImpl extends ElementAnnotator implements HgncAnnotator
         new AnnotatorOutputParameter(MiriamType.REFSEQ),
         new AnnotatorOutputParameter(MiriamType.UNIPROT),
         new AnnotatorOutputParameter(BioEntityField.SYMBOL),
+        new AnnotatorOutputParameter(BioEntityField.PREVIOUS_SYMBOLS),
         new AnnotatorOutputParameter(BioEntityField.SYNONYMS),
         new AnnotatorOutputParameter(BioEntityField.NAME),
         new AnnotatorOutputParameter(BioEntityField.FULL_NAME));
@@ -273,7 +274,7 @@ public class HgncAnnotatorImpl extends ElementAnnotator implements HgncAnnotator
       Node response = XmlParser.getNode("response", xml.getChildNodes());
       Node resultNode = XmlParser.getNode("result", response.getChildNodes());
       String count = XmlParser.getNodeAttr("numFound", resultNode);
-      if (count == null || count.equals("") || count.equals("0")) {
+      if (count == null || count.isEmpty() || count.equals("0")) {
         return result;
       } else {
         Node entry = XmlParser.getNode("doc", resultNode.getChildNodes());
@@ -332,7 +333,7 @@ public class HgncAnnotatorImpl extends ElementAnnotator implements HgncAnnotator
       Node response = XmlParser.getNode("response", xml.getChildNodes());
       Node resultNode = XmlParser.getNode("result", response.getChildNodes());
       String count = XmlParser.getNodeAttr("numFound", resultNode);
-      if (count == null || count.equals("") || count.equals("0")) {
+      if (count == null || count.isEmpty() || count.equals("0")) {
         return null;
       } else {
         Node entry = XmlParser.getNode("doc", resultNode.getChildNodes());
@@ -400,7 +401,7 @@ public class HgncAnnotatorImpl extends ElementAnnotator implements HgncAnnotator
       Node response = XmlParser.getNode("response", xml.getChildNodes());
       Node result = XmlParser.getNode("result", response.getChildNodes());
       String count = XmlParser.getNodeAttr("numFound", result);
-      if (count == null || count.equals("") || count.equals("0")) {
+      if (count == null || count.isEmpty() || count.equals("0")) {
         return null;
       } else {
         Node entry = XmlParser.getNode("doc", result.getChildNodes());
@@ -420,9 +421,7 @@ public class HgncAnnotatorImpl extends ElementAnnotator implements HgncAnnotator
         }
       }
       return null;
-    } catch (final IOException e) {
-      throw new AnnotatorException(e);
-    } catch (final InvalidXmlSchemaException e) {
+    } catch (final IOException | InvalidXmlSchemaException e) {
       throw new AnnotatorException(e);
     }
   }
diff --git a/annotation/src/test/java/lcsb/mapviewer/annotation/AnnotationNoTransactionTestFunctions.java b/annotation/src/test/java/lcsb/mapviewer/annotation/AnnotationNoTransactionTestFunctions.java
new file mode 100644
index 0000000000000000000000000000000000000000..43ecc94b43f82eccf880b0b854fd3b8a17afb516
--- /dev/null
+++ b/annotation/src/test/java/lcsb/mapviewer/annotation/AnnotationNoTransactionTestFunctions.java
@@ -0,0 +1,123 @@
+package lcsb.mapviewer.annotation;
+
+import lcsb.mapviewer.annotation.cache.GeneralCacheInterface;
+import lcsb.mapviewer.common.tests.TestUtils;
+import lcsb.mapviewer.common.tests.UnitTestFailedWatcher;
+import lcsb.mapviewer.converter.ConverterParams;
+import lcsb.mapviewer.converter.model.celldesigner.CellDesignerXmlParser;
+import lcsb.mapviewer.model.Project;
+import lcsb.mapviewer.model.graphics.HorizontalAlign;
+import lcsb.mapviewer.model.graphics.LineType;
+import lcsb.mapviewer.model.graphics.PolylineData;
+import lcsb.mapviewer.model.graphics.VerticalAlign;
+import lcsb.mapviewer.model.map.model.Model;
+import lcsb.mapviewer.model.map.model.ModelData;
+import lcsb.mapviewer.model.map.reaction.Reaction;
+import lcsb.mapviewer.model.map.species.GenericProtein;
+import lcsb.mapviewer.model.map.species.Ion;
+import lcsb.mapviewer.model.map.species.Species;
+import lcsb.mapviewer.model.user.User;
+import lcsb.mapviewer.persist.dao.cache.CacheTypeDao;
+import org.junit.Rule;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.context.ContextConfiguration;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+
+import java.util.HashMap;
+import java.util.Map;
+
+@ContextConfiguration(classes = SpringAnnotationTestConfig.class)
+@RunWith(SpringJUnit4ClassRunner.class)
+public abstract class AnnotationNoTransactionTestFunctions extends TestUtils {
+
+  private static final Map<String, Model> models = new HashMap<>();
+
+  @Rule
+  public UnitTestFailedWatcher unitTestFailedWatcher = new UnitTestFailedWatcher();
+
+  @Autowired
+  protected GeneralCacheInterface cache;
+
+  @Autowired
+  protected CacheTypeDao cacheTypeDao;
+
+  private static int idCounter = 0;
+
+  protected Model getModelForFile(final String fileName, final boolean fromCache) throws Exception {
+    if (!fromCache) {
+      logger.debug("File without cache: {}", fileName);
+      return new CellDesignerXmlParser().createModel(new ConverterParams().filename(fileName));
+    }
+    Model result = AnnotationNoTransactionTestFunctions.models.get(fileName);
+    if (result == null) {
+      logger.debug("File to cache: {}", fileName);
+
+      CellDesignerXmlParser parser = new CellDesignerXmlParser();
+      result = parser.createModel(new ConverterParams().filename(fileName).sizeAutoAdjust(false));
+      AnnotationNoTransactionTestFunctions.models.put(fileName, result);
+    }
+    return result;
+  }
+
+  public GenericProtein createProtein() {
+    GenericProtein result = new GenericProtein("s" + (idCounter++));
+    populateRandomData(result);
+
+    return result;
+  }
+
+  private static void populateRandomData(final Species result) {
+    result.setNameX(faker.number().numberBetween(10, 100));
+    result.setNameY(faker.number().numberBetween(10, 100));
+    result.setZ(faker.number().numberBetween(10, 100));
+    result.setNameWidth(faker.number().numberBetween(10, 100));
+    result.setNameHeight(faker.number().numberBetween(10, 100));
+    result.setHomodimer(faker.number().numberBetween(1, 10));
+    result.setNameHorizontalAlign(faker.options().option(HorizontalAlign.class));
+    result.setNameVerticalAlign(faker.options().option(VerticalAlign.class));
+    result.setHypothetical(faker.bool().bool());
+    result.setName(faker.name().fullName());
+  }
+
+  public Ion createIon() {
+    Ion result = new Ion("s" + (idCounter++));
+    populateRandomData(result);
+
+    return result;
+  }
+
+  public ModelData createModelData() {
+    ModelData model = new ModelData();
+    model.setWidth(faker.number().numberBetween(1, 1000));
+    model.setHeight(faker.number().numberBetween(1, 1000));
+
+    return model;
+  }
+
+  public Project createProject() {
+    User admin = new User();
+    admin.setId(BUILT_IN_TEST_ADMIN_ID);
+
+    Project project = new Project();
+    project.setProjectId(faker.numerify("T######"));
+    project.setOwner(admin);
+
+    return project;
+  }
+
+  protected Reaction createEmptyReaction() {
+    Reaction reaction = new Reaction(faker.numerify("re######"));
+    reaction.setLine(createLine());
+    reaction.setZ(faker.number().numberBetween(1, 10));
+
+    return reaction;
+  }
+
+  private PolylineData createLine() {
+    PolylineData line = new PolylineData();
+    line.setType(faker.options().option(LineType.class));
+    return line;
+  }
+
+}
diff --git a/annotation/src/test/java/lcsb/mapviewer/annotation/AnnotationTestFunctions.java b/annotation/src/test/java/lcsb/mapviewer/annotation/AnnotationTestFunctions.java
index 773ea51aca3896bd6bfa673b7273b961d0b6e4a7..1b385167c0db6ac5b6614c20a45e7401c9133534 100644
--- a/annotation/src/test/java/lcsb/mapviewer/annotation/AnnotationTestFunctions.java
+++ b/annotation/src/test/java/lcsb/mapviewer/annotation/AnnotationTestFunctions.java
@@ -1,95 +1,13 @@
 package lcsb.mapviewer.annotation;
 
-import lcsb.mapviewer.annotation.cache.GeneralCacheInterface;
-import lcsb.mapviewer.common.tests.TestUtils;
-import lcsb.mapviewer.common.tests.UnitTestFailedWatcher;
-import lcsb.mapviewer.converter.ConverterParams;
-import lcsb.mapviewer.converter.model.celldesigner.CellDesignerXmlParser;
-import lcsb.mapviewer.model.graphics.HorizontalAlign;
-import lcsb.mapviewer.model.graphics.VerticalAlign;
-import lcsb.mapviewer.model.map.model.Model;
-import lcsb.mapviewer.model.map.model.ModelData;
-import lcsb.mapviewer.model.map.species.GenericProtein;
-import lcsb.mapviewer.model.map.species.Ion;
-import lcsb.mapviewer.model.map.species.Species;
-import lcsb.mapviewer.persist.dao.cache.CacheTypeDao;
-import org.junit.Rule;
 import org.junit.runner.RunWith;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.test.context.ContextConfiguration;
 import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
 import org.springframework.transaction.annotation.Transactional;
 
-import java.util.HashMap;
-import java.util.Map;
-
 @Transactional
 @ContextConfiguration(classes = SpringAnnotationTestConfig.class)
 @RunWith(SpringJUnit4ClassRunner.class)
-public abstract class AnnotationTestFunctions extends TestUtils {
-
-  private static final Map<String, Model> models = new HashMap<>();
-
-  @Rule
-  public UnitTestFailedWatcher unitTestFailedWatcher = new UnitTestFailedWatcher();
-
-  @Autowired
-  protected GeneralCacheInterface cache;
-
-  @Autowired
-  protected CacheTypeDao cacheTypeDao;
-
-  private static int idCounter = 0;
-
-  protected Model getModelForFile(final String fileName, final boolean fromCache) throws Exception {
-    if (!fromCache) {
-      logger.debug("File without cache: {}", fileName);
-      return new CellDesignerXmlParser().createModel(new ConverterParams().filename(fileName));
-    }
-    Model result = AnnotationTestFunctions.models.get(fileName);
-    if (result == null) {
-      logger.debug("File to cache: {}", fileName);
-
-      CellDesignerXmlParser parser = new CellDesignerXmlParser();
-      result = parser.createModel(new ConverterParams().filename(fileName).sizeAutoAdjust(false));
-      AnnotationTestFunctions.models.put(fileName, result);
-    }
-    return result;
-  }
-
-  public GenericProtein createProtein() {
-    GenericProtein result = new GenericProtein("s" + (idCounter++));
-    populateRandomData(result);
-
-    return result;
-  }
-
-  private static void populateRandomData(final Species result) {
-    result.setNameX(faker.number().numberBetween(10, 100));
-    result.setNameY(faker.number().numberBetween(10, 100));
-    result.setZ(faker.number().numberBetween(10, 100));
-    result.setNameWidth(faker.number().numberBetween(10, 100));
-    result.setNameHeight(faker.number().numberBetween(10, 100));
-    result.setHomodimer(faker.number().numberBetween(1, 10));
-    result.setNameHorizontalAlign(faker.options().option(HorizontalAlign.class));
-    result.setNameVerticalAlign(faker.options().option(VerticalAlign.class));
-    result.setHypothetical(faker.bool().bool());
-    result.setName(faker.name().fullName());
-  }
-
-  public Ion createIon() {
-    Ion result = new Ion("s" + (idCounter++));
-    populateRandomData(result);
-
-    return result;
-  }
-
-  public ModelData createModelData() {
-    ModelData model = new ModelData();
-    model.setWidth(faker.number().numberBetween(1, 1000));
-    model.setHeight(faker.number().numberBetween(1, 1000));
-
-    return model;
-  }
+public abstract class AnnotationTestFunctions extends AnnotationNoTransactionTestFunctions {
 
 }
diff --git a/annotation/src/test/java/lcsb/mapviewer/annotation/services/ModelAnnotatorTest.java b/annotation/src/test/java/lcsb/mapviewer/annotation/services/ModelAnnotatorTest.java
index 4c0b8d8d3b740145734cf83af26ac7ec29461195..ee55829d8da801de411ff11c1b4c98622c79bc04 100644
--- a/annotation/src/test/java/lcsb/mapviewer/annotation/services/ModelAnnotatorTest.java
+++ b/annotation/src/test/java/lcsb/mapviewer/annotation/services/ModelAnnotatorTest.java
@@ -1,16 +1,17 @@
 package lcsb.mapviewer.annotation.services;
 
-import lcsb.mapviewer.annotation.AnnotationTestFunctions;
+import lcsb.mapviewer.annotation.AnnotationNoTransactionTestFunctions;
 import lcsb.mapviewer.annotation.services.annotators.IElementAnnotator;
 import lcsb.mapviewer.common.IProgressUpdater;
+import lcsb.mapviewer.model.Project;
 import lcsb.mapviewer.model.map.BioEntity;
 import lcsb.mapviewer.model.map.MiriamData;
 import lcsb.mapviewer.model.map.MiriamType;
 import lcsb.mapviewer.model.map.model.Model;
+import lcsb.mapviewer.model.map.model.ModelData;
 import lcsb.mapviewer.model.map.model.ModelFullIndexed;
 import lcsb.mapviewer.model.map.model.ModelSubmodelConnection;
 import lcsb.mapviewer.model.map.model.SubmodelType;
-import lcsb.mapviewer.model.map.reaction.Reaction;
 import lcsb.mapviewer.model.map.species.Element;
 import lcsb.mapviewer.model.map.species.GenericProtein;
 import lcsb.mapviewer.model.map.species.Ion;
@@ -18,6 +19,8 @@ import lcsb.mapviewer.model.map.species.Protein;
 import lcsb.mapviewer.model.map.species.Species;
 import lcsb.mapviewer.model.user.UserAnnotationSchema;
 import lcsb.mapviewer.model.user.UserClassAnnotators;
+import lcsb.mapviewer.persist.DbUtils;
+import lcsb.mapviewer.persist.dao.ProjectDao;
 import lcsb.mapviewer.persist.dao.map.ModelDao;
 import org.apache.commons.lang3.mutable.MutableDouble;
 import org.junit.After;
@@ -38,11 +41,8 @@ import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertNull;
 import static org.junit.Assert.assertTrue;
 
-public class ModelAnnotatorTest extends AnnotationTestFunctions {
-  private final IProgressUpdater updater = new IProgressUpdater() {
-    @Override
-    public void setProgress(final double progress) {
-    }
+public class ModelAnnotatorTest extends AnnotationNoTransactionTestFunctions {
+  private final IProgressUpdater updater = progress -> {
   };
 
   @Autowired
@@ -51,50 +51,72 @@ public class ModelAnnotatorTest extends AnnotationTestFunctions {
   @Autowired
   private ModelDao modelDao;
 
+  @Autowired
+  private ProjectDao projectDao;
+
+  @Autowired
+  private DbUtils dbUtils;
+
+  private List<Project> projects = new ArrayList<>();
+
   @Before
   public void setUp() throws Exception {
+    projects = new ArrayList<>();
   }
 
   @After
   public void tearDown() throws Exception {
+    for (Project project : projects) {
+      dbUtils.callInTransaction(() -> {
+        projectDao.delete(projectDao.getById(project.getId()));
+        return null;
+      });
+    }
   }
 
   @Test
   public void testAnnotateModel() {
-    Model model = new ModelFullIndexed(null);
-
-    model.addReaction(new Reaction("re"));
+    ModelData modelData = createModelData();
+    modelData.addReaction(createEmptyReaction());
 
-    Species protein1 = new GenericProtein("a1");
+    Species protein1 = createProtein();
     protein1.setName("SNCA");
 
-    Species protein2 = new GenericProtein("a2");
+    Species protein2 = createProtein();
     protein2.setName("PDK1");
     protein2.addMiriamData(new MiriamData(MiriamType.CAS, "c"));
 
-    model.addElement(protein1);
-    model.addElement(protein2);
+    modelData.addElement(protein1);
+    modelData.addElement(protein2);
+    Project project = createProject();
+    project.addModel(modelData);
+    this.persistProject(project);
 
-    modelAnnotator.annotateModel(model, updater, modelAnnotator.createDefaultAnnotatorSchema());
+    modelAnnotator.performAnnotations(project, updater, modelAnnotator.createDefaultAnnotatorSchema());
 
-    assertTrue(model.getElementByElementId("a1").getMiriamData().size() > 0);
-    assertTrue(model.getElementByElementId("a2").getMiriamData().size() >= 1);
+    dbUtils.callInTransaction(() -> {
+      Model model = new ModelFullIndexed(modelDao.getById(modelData.getId()));
+      assertFalse(model.getElementByElementId(protein1.getElementId()).getMiriamData().isEmpty());
+      assertFalse(model.getElementByElementId(protein2.getElementId()).getMiriamData().isEmpty());
 
-    modelAnnotator.annotateModel(model, updater, modelAnnotator.createDefaultAnnotatorSchema());
+      return null;
+    });
+    modelAnnotator.performAnnotations(project, updater, modelAnnotator.createDefaultAnnotatorSchema());
   }
 
+
   @Test
-  public void testAnnotateModelWithGivenAnnotators() throws Exception {
-    Model model = new ModelFullIndexed(null);
-    GenericProtein species = new GenericProtein("id1");
+  public void testAnnotateModelWithGivenAnnotators() {
+    ModelData modelData = createModelData();
+    GenericProtein species = createProtein();
     species.setName("SNCA");
-    model.addElement(species);
+    modelData.addElement(species);
 
-    Ion species2 = new Ion("id2");
+    Ion species2 = createIon();
     species2.setName("h2o");
     species2.addMiriamData(new MiriamData(MiriamType.CHEBI, "12345"));
-    model.addElement(species2);
-    model.addReaction(new Reaction("re"));
+    modelData.addElement(species2);
+    modelData.addReaction(createEmptyReaction());
 
     UserAnnotationSchema annotationSchema = new UserAnnotationSchema();
 
@@ -106,140 +128,196 @@ public class ModelAnnotatorTest extends AnnotationTestFunctions {
     ionAnnotators.setAnnotators(modelAnnotator.getDefaultAnnotators(Ion.class));
     annotationSchema.addClassAnnotator(ionAnnotators);
 
-    modelAnnotator.annotateModel(model, updater, annotationSchema);
+    Project project = createProject();
+    project.addModel(modelData);
+    this.persistProject(project);
 
-    // we didn't annotate protein
-    assertEquals(0, species.getMiriamData().size());
-    // but we should annotate ion
-    assertFalse(species2.getFullName().isEmpty());
+    modelAnnotator.performAnnotations(project, updater, annotationSchema);
+
+    dbUtils.callInTransaction(() -> {
+      Model model = new ModelFullIndexed(modelDao.getById(modelData.getId()));
+
+      Element protein = model.getElementByElementId(species.getElementId());
+      Element ion = model.getElementByElementId(species2.getElementId());
+      // we didn't annotate protein
+      assertEquals(0, protein.getMiriamData().size());
+      // but we should annotate ion
+      assertFalse(ion.getFullName().isEmpty());
+
+      return null;
+    });
   }
 
   @Test
   public void testDuplicateAnnotations() throws Exception {
-    int counter = 0;
-    Model model = getModelForFile("testFiles/annotation/duplicate.xml", false);
+    ModelData modelData = getModelForFile("testFiles/annotation/duplicate.xml", false).getModelData();
 
-    Set<String> knowAnnotations = new HashSet<String>();
+    Project project = createProject();
+    project.addModel(modelData);
+    this.persistProject(project);
 
-    modelAnnotator.annotateModel(model, updater, modelAnnotator.createDefaultAnnotatorSchema());
-    for (final MiriamData md : model.getElementByElementId("sa1").getMiriamData()) {
-      knowAnnotations.add(md.getDataType() + ":" + md.getRelationType() + ":" + md.getResource());
-    }
-    counter = 0;
-    for (final String string : knowAnnotations) {
-      if (string.contains("29108")) {
-        counter++;
+    modelAnnotator.performAnnotations(project, updater, modelAnnotator.createDefaultAnnotatorSchema());
+
+    dbUtils.callInTransaction(() -> {
+      Model model = new ModelFullIndexed(modelDao.getById(project.getTopModel().getId()));
+      Set<String> knowAnnotations = new HashSet<>();
+      for (final MiriamData md : model.getElementByElementId("sa1").getMiriamData()) {
+        knowAnnotations.add(md.getDataType() + ":" + md.getRelationType() + ":" + md.getResource());
       }
-    }
-    assertEquals(1, counter);
+      int counter = 0;
+      for (final String string : knowAnnotations) {
+        if (string.contains("29108")) {
+          counter++;
+        }
+      }
+      assertEquals(1, counter);
+      return null;
+    });
 
-    modelAnnotator.annotateModel(model, updater, modelAnnotator.createDefaultAnnotatorSchema());
+    modelAnnotator.performAnnotations(project, updater, modelAnnotator.createDefaultAnnotatorSchema());
 
-    knowAnnotations.clear();
-    for (final MiriamData md : model.getElementByElementId("sa1").getMiriamData()) {
-      knowAnnotations.add(md.getDataType() + ":" + md.getRelationType() + ":" + md.getResource());
-    }
-    counter = 0;
-    for (final String string : knowAnnotations) {
-      if (string.contains("29108")) {
-        counter++;
+    dbUtils.callInTransaction(() -> {
+      Model model = new ModelFullIndexed(modelDao.getById(project.getTopModel().getId()));
+      Set<String> knowAnnotations = new HashSet<>();
+      for (final MiriamData md : model.getElementByElementId("sa1").getMiriamData()) {
+        knowAnnotations.add(md.getDataType() + ":" + md.getRelationType() + ":" + md.getResource());
       }
-    }
-    assertEquals(1, counter);
+      int counter = 0;
+      for (final String string : knowAnnotations) {
+        if (string.contains("29108")) {
+          counter++;
+        }
+      }
+      assertEquals(1, counter);
+      return null;
+    });
   }
 
   @Test
   public void testGetAnnotationsForSYN() throws Exception {
-    Model model = getModelForFile("testFiles/annotation/emptyAnnotationsSyn1.xml", true);
-    Element sa1 = model.getElementByElementId("sa1");
-    Element sa2 = model.getElementByElementId("sa2");
-
-    assertFalse(sa1.getNotes().contains("Symbol"));
-    assertFalse(sa2.getNotes().contains("Symbol"));
-    modelAnnotator.annotateModel(model, updater, modelAnnotator.createDefaultAnnotatorSchema());
-    assertFalse(sa2.getNotes().contains("Symbol"));
-    assertNotNull(sa1.getSymbol());
-    assertNotEquals("", sa1.getSymbol());
-    // modelAnnotator.removeIncorrectAnnotations(model, updater);
-    assertNotNull(sa1.getSymbol());
-    assertNotEquals("", sa1.getSymbol());
-    assertFalse(sa1.getNotes().contains("Symbol"));
-    assertFalse(sa2.getNotes().contains("Symbol"));
-    assertNull(sa2.getSymbol());
-
-    for (final Species el : model.getSpeciesList()) {
-      if (el.getNotes() != null) {
-        assertFalse("Invalid notes: " + el.getNotes(), el.getNotes().contains("Symbol"));
-        assertFalse("Invalid notes: " + el.getNotes(), el.getNotes().contains("HGNC"));
+    ModelData modelData = getModelForFile("testFiles/annotation/emptyAnnotationsSyn1.xml", true).getModelData();
+
+    Project project = createProject();
+    project.addModel(modelData);
+    this.persistProject(project);
+    modelAnnotator.performAnnotations(project, updater, modelAnnotator.createDefaultAnnotatorSchema());
+
+    dbUtils.callInTransaction(() -> {
+      Project p = projectDao.getProjectByProjectId(project.getProjectId());
+      Model model = new ModelFullIndexed(modelDao.getById(p.getTopModel().getId()));
+
+      Element sa1 = model.getElementByElementId("sa1");
+      Element sa2 = model.getElementByElementId("sa2");
+
+      assertFalse(sa2.getNotes().contains("Symbol"));
+      assertNotNull(sa1.getSymbol());
+      assertNotEquals("", sa1.getSymbol());
+      // modelAnnotator.removeIncorrectAnnotations(model, updater);
+      assertNotNull(sa1.getSymbol());
+      assertNotEquals("", sa1.getSymbol());
+      assertFalse(sa1.getNotes().contains("Symbol"));
+      assertFalse(sa2.getNotes().contains("Symbol"));
+      assertNull(sa2.getSymbol());
+
+      for (final Species el : model.getSpeciesList()) {
+        if (el.getNotes() != null) {
+          assertFalse("Invalid notes: " + el.getNotes(), el.getNotes().contains("Symbol"));
+          assertFalse("Invalid notes: " + el.getNotes(), el.getNotes().contains("HGNC"));
+        }
       }
-    }
+      return null;
+    });
+
   }
 
   @Test
   public void testAnnotateModelWithDrugMolecule() throws Exception {
-    Model model = super.getModelForFile("testFiles/annotation/problematic.xml", false);
+    ModelData modelData = super.getModelForFile("testFiles/annotation/problematic.xml", false).getModelData();
+
+    Project project = createProject();
+    project.addModel(modelData);
+    this.persistProject(project);
 
-    modelAnnotator.annotateModel(model, updater, modelAnnotator.createDefaultAnnotatorSchema());
-    modelDao.add(model);
+    modelAnnotator.performAnnotations(project, updater, modelAnnotator.createDefaultAnnotatorSchema());
 
-    modelDao.delete(model);
   }
 
+
   @Test
-  public void testGetDefaultRequired() throws Exception {
+  public void testGetDefaultRequired() {
     Map<Class<? extends BioEntity>, Set<MiriamType>> map1 = modelAnnotator.getDefaultRequiredClasses();
     assertNotNull(map1);
   }
 
   @Test
-  public void testGetDefaultValid() throws Exception {
+  public void testGetDefaultValid() {
     Map<Class<? extends BioEntity>, Set<MiriamType>> map2 = modelAnnotator.getDefaultValidClasses();
     assertNotNull(map2);
   }
 
   @Test
-  public void testPerformAnnotationAndCheckProgress() throws Exception {
-    Model model = new ModelFullIndexed(null);
-    Model submodel = new ModelFullIndexed(null);
-    Model submodel2 = new ModelFullIndexed(null);
+  public void testPerformAnnotationAndCheckProgress() {
+    dbUtils.callInTransaction(() -> {
+      Project project = createProject();
+      Model model = new ModelFullIndexed(null);
+      Model submodel = new ModelFullIndexed(null);
+      Model submodel2 = new ModelFullIndexed(null);
 
-    GenericProtein protein = new GenericProtein("el");
+      GenericProtein protein = createProtein();
 
-    model.addSubmodelConnection(new ModelSubmodelConnection(submodel, SubmodelType.UNKNOWN));
-    model.addSubmodelConnection(new ModelSubmodelConnection(submodel2, SubmodelType.UNKNOWN));
+      model.addSubmodelConnection(new ModelSubmodelConnection(submodel, SubmodelType.UNKNOWN));
+      model.addSubmodelConnection(new ModelSubmodelConnection(submodel2, SubmodelType.UNKNOWN));
 
-    model.addElement(protein);
-    submodel.addElement(protein);
-    submodel2.addElement(protein);
+      model.addElement(protein);
+      submodel.addElement(protein);
+      submodel2.addElement(protein);
 
-    final MutableDouble maxProgress = new MutableDouble(0.0);
+      final MutableDouble maxProgress = new MutableDouble(0.0);
 
-    modelAnnotator.performAnnotations(model, new IProgressUpdater() {
-      @Override
-      public void setProgress(final double progress) {
-        maxProgress.setValue(Math.max(progress, maxProgress.getValue()));
-      }
-    });
-    assertTrue(maxProgress.getValue() <= IProgressUpdater.MAX_PROGRESS);
+      project.addModel(model);
+      project.addModel(submodel);
+      project.addModel(submodel2);
+      projectDao.add(project);
+
+
+      modelAnnotator.performAnnotations(project,
+          progress -> maxProgress.setValue(Math.max(progress, maxProgress.getValue())), new UserAnnotationSchema());
+      assertTrue(maxProgress.getValue() <= IProgressUpdater.MAX_PROGRESS);
 
+      projectDao.delete(project);
+
+      return null;
+    });
   }
 
   @Test
   public void testGetAvailableAnnotators() {
     List<IElementAnnotator> list = modelAnnotator.getAvailableAnnotators(Protein.class);
-    assertTrue(list.size() > 0);
+    assertFalse(list.isEmpty());
   }
 
   @Test
   public void testGetAvailableAnnotators2() {
     List<IElementAnnotator> list = modelAnnotator.getAvailableAnnotators();
-    assertTrue(list.size() > 0);
+    assertFalse(list.isEmpty());
   }
 
   @Test
   public void testGetAvailableDefaultAnnotators() {
     List<IElementAnnotator> list = modelAnnotator.getAvailableDefaultAnnotators(Protein.class);
-    assertTrue(list.size() > 0);
+    assertFalse(list.isEmpty());
+  }
+
+  protected Project persistProject(final Project project) {
+    Project projectData = dbUtils.callInTransactionWithResult(() -> {
+      try {
+        return projectDao.add(project);
+      } catch (Exception e) {
+        throw new RuntimeException(e);
+      }
+    });
+    projects.add(projectData);
+    return projectData;
   }
 
 }
diff --git a/annotation/src/test/java/lcsb/mapviewer/annotation/services/annotators/HgncAnnotatorTest.java b/annotation/src/test/java/lcsb/mapviewer/annotation/services/annotators/HgncAnnotatorTest.java
index d0b2c8083a06fe301aa485c9dda95ff0cf7a4c97..513373614491627c0adec42d51e30c57d0a48dcb 100644
--- a/annotation/src/test/java/lcsb/mapviewer/annotation/services/annotators/HgncAnnotatorTest.java
+++ b/annotation/src/test/java/lcsb/mapviewer/annotation/services/annotators/HgncAnnotatorTest.java
@@ -1,24 +1,5 @@
 package lcsb.mapviewer.annotation.services.annotators;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertFalse;
-import static org.junit.Assert.assertNotNull;
-import static org.junit.Assert.assertNull;
-import static org.junit.Assert.assertTrue;
-import static org.mockito.ArgumentMatchers.anyString;
-import static org.mockito.ArgumentMatchers.nullable;
-import static org.mockito.Mockito.when;
-
-import java.io.IOException;
-import java.util.List;
-
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-import org.mockito.Mockito;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.test.util.ReflectionTestUtils;
-
 import lcsb.mapviewer.annotation.AnnotationTestFunctions;
 import lcsb.mapviewer.annotation.cache.GeneralCacheInterface;
 import lcsb.mapviewer.annotation.cache.WebPageDownloader;
@@ -28,6 +9,24 @@ import lcsb.mapviewer.model.map.MiriamData;
 import lcsb.mapviewer.model.map.MiriamType;
 import lcsb.mapviewer.model.map.species.GenericProtein;
 import lcsb.mapviewer.model.map.species.Species;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.test.util.ReflectionTestUtils;
+
+import java.io.IOException;
+import java.util.List;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.nullable;
+import static org.mockito.Mockito.when;
 
 public class HgncAnnotatorTest extends AnnotationTestFunctions {
 
@@ -37,7 +36,7 @@ public class HgncAnnotatorTest extends AnnotationTestFunctions {
   @Autowired
   private GeneralCacheInterface cache;
 
-  private WebPageDownloader webPageDownloader = new WebPageDownloader();
+  private final WebPageDownloader webPageDownloader = new WebPageDownloader();
 
   @Before
   public void setUp() {
@@ -112,10 +111,10 @@ public class HgncAnnotatorTest extends AnnotationTestFunctions {
     proteinAlias.addMiriamData(nsmf);
     hgncAnnotator.annotateElement(proteinAlias);
     assertNotNull(proteinAlias.getSymbol());
-    assertTrue(proteinAlias.getFormerSymbols().size() > 0);
+    assertFalse(proteinAlias.getFormerSymbols().isEmpty());
     assertNotNull(proteinAlias.getFullName());
     assertTrue(proteinAlias.getMiriamData().size() > 1);
-    assertTrue(proteinAlias.getSynonyms().size() > 0);
+    assertFalse(proteinAlias.getSynonyms().isEmpty());
 
     boolean ensemble = false;
     boolean hgncId = false;
@@ -314,13 +313,13 @@ public class HgncAnnotatorTest extends AnnotationTestFunctions {
     MiriamData data1 = new MiriamData(MiriamType.HGNC_SYMBOL, "PTGS1");
     MiriamData entrez = hgncAnnotator.hgncToEntrez(data1);
     assertNotNull(entrez);
-    assertTrue(new MiriamData(MiriamType.ENTREZ, "5742").equals(entrez));
+    assertEquals(new MiriamData(MiriamType.ENTREZ, "5742"), entrez);
 
     // check by id
     data1 = new MiriamData(MiriamType.HGNC, "11138");
     entrez = hgncAnnotator.hgncToEntrez(data1);
     assertNotNull(entrez);
-    assertTrue(new MiriamData(MiriamType.ENTREZ, "6622").equals(entrez));
+    assertEquals(new MiriamData(MiriamType.ENTREZ, "6622"), entrez);
   }
 
   @Test
diff --git a/commons/src/main/java/lcsb/mapviewer/common/tests/TestUtils.java b/commons/src/main/java/lcsb/mapviewer/common/tests/TestUtils.java
index 13abad157870eb17f7a4e97a1b2e252b7c15f469..9b7ee6fecb51e8095505a0712b77d5aa6d8a9af4 100644
--- a/commons/src/main/java/lcsb/mapviewer/common/tests/TestUtils.java
+++ b/commons/src/main/java/lcsb/mapviewer/common/tests/TestUtils.java
@@ -61,6 +61,8 @@ public class TestUtils {
   protected static final String BUILT_IN_TEST_ADMIN_PASSWORD = "admin";
   protected static final String BUILT_IN_TEST_ADMIN_LOGIN = "admin";
 
+  protected static final int BUILT_IN_TEST_ADMIN_ID = 1;
+
   protected static final String BUILT_IN_PROJECT = "empty";
   protected static final int BUILT_IN_MAP_ID = 3;
 
diff --git a/converter-graphics/src/main/java/lcsb/mapviewer/converter/graphics/MapGenerator.java b/converter-graphics/src/main/java/lcsb/mapviewer/converter/graphics/MapGenerator.java
index 6a153c0cdd3fc754a1f0c164583df9d66217427a..eb8e9b4f61bfbd83213b3026e80f23c8c5532f5b 100644
--- a/converter-graphics/src/main/java/lcsb/mapviewer/converter/graphics/MapGenerator.java
+++ b/converter-graphics/src/main/java/lcsb/mapviewer/converter/graphics/MapGenerator.java
@@ -1,25 +1,24 @@
 package lcsb.mapviewer.converter.graphics;
 
-import java.io.File;
-import java.io.IOException;
-
-import org.apache.commons.io.FileUtils;
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-
 import lcsb.mapviewer.common.Configuration;
 import lcsb.mapviewer.common.IProgressUpdater;
 import lcsb.mapviewer.converter.graphics.AbstractImageGenerator.Params;
 import lcsb.mapviewer.model.map.layout.ProjectBackground;
 import lcsb.mapviewer.model.map.layout.ProjectBackgroundImageLayer;
 import lcsb.mapviewer.model.map.model.Model;
+import lcsb.mapviewer.model.map.model.ModelData;
+import org.apache.commons.io.FileUtils;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.io.File;
+import java.io.IOException;
 
 /**
  * This class allows to generate set of images that could be later on used by
  * Google Maps API.
- * 
+ *
  * @author Piotr Gawron
- * 
  */
 public class MapGenerator {
 
@@ -42,17 +41,14 @@ public class MapGenerator {
   /**
    * Default class logger.
    */
-  private static Logger logger = LogManager.getLogger();
+  private static final Logger logger = LogManager.getLogger();
 
   /**
    * This method generates PNG images used by GoogleMaps API.
    *
-   * @param params
-   *          {@link MapGeneratorParams params} used for generating images
-   * @throws IOException
-   *           exception thrown when there are some problems with creating images
-   * @throws DrawingException
-   *           thrown when there was a problem with drawing a map
+   * @param params {@link MapGeneratorParams params} used for generating images
+   * @throws IOException      exception thrown when there are some problems with creating images
+   * @throws DrawingException thrown when there was a problem with drawing a map
    */
   public void generateMapImages(final MapGeneratorParams params) throws IOException, DrawingException {
 
@@ -137,32 +133,28 @@ public class MapGenerator {
   /**
    * Computes how many zoom levels should exists for the map model.
    *
-   * @param model
-   *          map model
+   * @param model map model
    * @return zoom levels for the model
    */
-  public int computeZoomLevels(final Model model) {
+  public int computeZoomLevels(final ModelData model) {
     return (int) Math.ceil(Math.log(computeZoomFactor(model)) / Math.log(2));
   }
 
   /**
    * Computes the scale that should be used on the top zoom level.
    *
-   * @param model
-   *          model for which computation is done
+   * @param model model for which computation is done
    * @return scale on the top zoom level
    */
-  public double computeZoomFactor(final Model model) {
+  public double computeZoomFactor(final ModelData model) {
     return Math.max(model.getHeight(), model.getWidth()) / (TILE_SIZE);
   }
 
   /**
    * Removes files that were generated for the layout.
    *
-   * @param background
-   *          layout object for which images will be removed
-   * @throws IOException
-   *           thrown when there are some problems with removing files
+   * @param background layout object for which images will be removed
+   * @throws IOException thrown when there are some problems with removing files
    */
   public void removeProjectBackground(final ProjectBackground background) throws IOException {
     removeProjectBackground(background, null);
@@ -171,12 +163,9 @@ public class MapGenerator {
   /**
    * Removes files that were generated for the layout.
    *
-   * @param background
-   *          background for which images will be removed
-   * @param homeDir
-   *          determines the directory where images are stored (it's optional)
-   * @throws IOException
-   *           thrown when there are some problems with removing files
+   * @param background background for which images will be removed
+   * @param homeDir    determines the directory where images are stored (it's optional)
+   * @throws IOException thrown when there are some problems with removing files
    */
   public void removeProjectBackground(final ProjectBackground background, final String homeDir) throws IOException {
     for (final ProjectBackgroundImageLayer imageLayer : background.getProjectBackgroundImageLayer()) {
@@ -205,36 +194,29 @@ public class MapGenerator {
    * <li>{@link #levels}</li>.
    * </ul>
    *
-   *
    * @author Piotr Gawron
-   *
    */
   public class MapGeneratorParams {
     /**
      * Information about map to be generated.
-     *
      */
     private Model model;
     /**
      * Where we want to put the files. The structure of the directory will be as
      * follows: directory/subDirA/subDirB/file.PNG. subDirA is a level of zooming.
      * subDirB is x coordinate of the image. file is y coordinate of the image.
-     *
      */
     private String directory;
     /**
      * Do we want to generate images in hierarchical view or normal.
-     *
      */
     private boolean nested = false;
     /**
      * Should we remove empty elements in nested view.
-     *
      */
     private boolean removeEmpty = false;
     /**
      * Callback function that update progress information.
-     *
      */
     private IProgressUpdater updater = new IProgressUpdater() {
       @Override
@@ -258,27 +240,25 @@ public class MapGenerator {
     private boolean sbgn;
 
     /**
-     * @param model
-     *          the model to set
-     * @see #model
+     * @param model the model to set
      * @return full {@link MapGeneratorParams} object
+     * @see #model
      */
     public MapGeneratorParams model(final Model model) {
       this.model = model;
       if (zoomFactor == null) {
-        zoomFactor = computeZoomFactor(model);
+        zoomFactor = computeZoomFactor(model.getModelData());
       }
       if (levels == null) {
-        levels = computeZoomLevels(model);
+        levels = computeZoomLevels(model.getModelData());
       }
       return this;
     }
 
     /**
-     * @param directory
-     *          the directory to set
-     * @see #directory
+     * @param directory the directory to set
      * @return full {@link MapGeneratorParams} object
+     * @see #directory
      */
     public MapGeneratorParams directory(final String directory) {
       this.directory = directory;
@@ -294,10 +274,9 @@ public class MapGenerator {
     }
 
     /**
-     * @param nested
-     *          the nested to set
-     * @see #nested
+     * @param nested the nested to set
      * @return full {@link MapGeneratorParams} object
+     * @see #nested
      */
     public MapGeneratorParams nested(final boolean nested) {
       this.nested = nested;
@@ -313,10 +292,9 @@ public class MapGenerator {
     }
 
     /**
-     * @param removeEmpty
-     *          the removeEmpty to set
-     * @see #removeEmpty
+     * @param removeEmpty the removeEmpty to set
      * @return full {@link MapGeneratorParams} object
+     * @see #removeEmpty
      */
     public MapGeneratorParams removeEmpty(final boolean removeEmpty) {
       this.removeEmpty = removeEmpty;
@@ -332,10 +310,9 @@ public class MapGenerator {
     }
 
     /**
-     * @param updater
-     *          the updater to set
-     * @see #updater
+     * @param updater the updater to set
      * @return full {@link MapGeneratorParams} object
+     * @see #updater
      */
     public MapGeneratorParams updater(final IProgressUpdater updater) {
       this.updater = updater;
@@ -351,10 +328,9 @@ public class MapGenerator {
     }
 
     /**
-     * @param zoomFactor
-     *          the zoomFactor to set
-     * @see #zoomFactor
+     * @param zoomFactor the zoomFactor to set
      * @return full {@link MapGeneratorParams} object
+     * @see #zoomFactor
      */
     public MapGeneratorParams zoomFactor(final Double zoomFactor) {
       this.zoomFactor = zoomFactor;
@@ -370,10 +346,9 @@ public class MapGenerator {
     }
 
     /**
-     * @param levels
-     *          the levels to set
-     * @see #levels
+     * @param levels the levels to set
      * @return full {@link MapGeneratorParams} object
+     * @see #levels
      */
     public MapGeneratorParams levels(final Integer levels) {
       this.levels = levels;
@@ -389,10 +364,9 @@ public class MapGenerator {
     }
 
     /**
-     * @param sbgn
-     *          the sbgn to set
-     * @see #sbgn
+     * @param sbgn the sbgn to set
      * @return object with all parameters
+     * @see #sbgn
      */
     public MapGeneratorParams sbgn(final boolean sbgn) {
       this.sbgn = sbgn;
diff --git a/frontend-js/src/main/js/ServerConnector.js b/frontend-js/src/main/js/ServerConnector.js
index 9e6b35a1bd898e7935c56b20e785814eda2c2749..8965fe3a575b33146f5be0df4bea5ae4b33aa85b 100644
--- a/frontend-js/src/main/js/ServerConnector.js
+++ b/frontend-js/src/main/js/ServerConnector.js
@@ -474,6 +474,20 @@ ServerConnector.getApiUrl = function (paramObj) {
   return result;
 };
 
+ServerConnector.getNewApiUrl = function (paramObj) {
+  var type = paramObj.type;
+  var params = this.createGetParams(paramObj.params);
+
+  var result = paramObj.url;
+  if (result === undefined) {
+    result = this.getNewApiBaseUrl() + type;
+  }
+  if (params !== "") {
+    result += "?" + params;
+  }
+  return result;
+};
+
 /**
  *
  * @param [queryParams]
@@ -487,6 +501,19 @@ ServerConnector.getProjectsUrl = function (queryParams, filterParams) {
   });
 };
 
+/**
+ *
+ * @param [queryParams]
+ * @param [filterParams]
+ * @return {string}
+ */
+ServerConnector.getNewApiProjectsUrl = function (queryParams, filterParams) {
+  return this.getNewApiUrl({
+    type: "projects/",
+    params: filterParams
+  });
+};
+
 ServerConnector.getPluginsUrl = function (queryParams, filterParams) {
   return this.getApiUrl({
     type: "plugins/",
@@ -538,6 +565,14 @@ ServerConnector.getProjectUrl = function (queryParams, filterParams) {
   });
 };
 
+ServerConnector.getNewApiProjectUrl = function (queryParams, filterParams) {
+  var id = this.getIdOrAsterisk(queryParams.projectId);
+  return this.getNewApiUrl({
+    url: this.getNewApiProjectsUrl(queryParams) + id + "/",
+    params: filterParams
+  });
+};
+
 ServerConnector.getMoveProjectUrl = function (queryParams, filterParams) {
   var id = this.getIdOrAsterisk(queryParams.projectId);
   return this.getNewApiBaseUrl() + 'projects/' + id + ':move';
@@ -620,6 +655,17 @@ ServerConnector.getProjectLogsUrl = function (queryParams, filterParams) {
     params: filterParams
   });
 };
+
+ServerConnector.getProjectJobsUrl = function (queryParams, filterParams) {
+  filterParams.page = filterParams.page || 0;
+  filterParams.size = filterParams.size || 10;
+
+  return this.getNewApiUrl({
+    url: this.getNewApiProjectUrl(queryParams) + "job/",
+    params: filterParams
+  });
+};
+
 ServerConnector.getMeshUrl = function (queryParams, filterParams) {
   return this.getApiUrl({
     type: "mesh/" + queryParams.id,
@@ -3301,6 +3347,33 @@ ServerConnector.getProjectLogs = function (params) {
     return JSON.parse(content);
   });
 };
+/**
+ *
+ * @param {Object} [params]
+ * @param {number} [params.start]
+ * @param {number} [params.length]
+ * @param {string} [params.projectId]
+ * @return {Promise}
+ */
+ServerConnector.getProjectJobs = function (params) {
+  var self = this;
+  if (params === undefined) {
+    params = {};
+  }
+
+  var queryParams = {};
+  var filterParams = {
+    page: params.start,
+    size: params.length
+  };
+  return self.getProjectId(params.projectId).then(function (result) {
+    queryParams.projectId = result;
+    return self.sendGetRequest(self.getProjectJobsUrl(queryParams, filterParams));
+  }).then(function (content) {
+    return JSON.parse(content);
+  });
+};
+
 
 /**
  *
diff --git a/frontend-js/src/main/js/gui/admin/JobListDialog.js b/frontend-js/src/main/js/gui/admin/JobListDialog.js
new file mode 100644
index 0000000000000000000000000000000000000000..77716074b01af11f18a8ca844206efa8463b7d99
--- /dev/null
+++ b/frontend-js/src/main/js/gui/admin/JobListDialog.js
@@ -0,0 +1,216 @@
+"use strict";
+
+var Promise = require("bluebird");
+var $ = require('jquery');
+
+/* exported logger */
+
+var AbstractGuiElement = require('../AbstractGuiElement');
+
+var Functions = require('../../Functions');
+var GuiConnector = require('../../GuiConnector');
+
+
+/**
+ *
+ * @param {Object} params
+ * @param {HTMLElement} params.element
+ * @param {CustomMap} [params.customMap]
+ * @param {Configuration} params.configuration
+ * @param {Project} [params.project]
+ * @param {string} params.projectId
+ * @param {ServerConnector} params.serverConnector
+ *
+ * @constructor
+ * @extends AbstractGuiElement
+ */
+function JobListDialog(params) {
+  AbstractGuiElement.call(this, params);
+  var self = this;
+  self.createJobListDialogGui();
+
+  /**
+   * @type {string}
+   * @private
+   */
+  self._projectId = params.projectId;
+}
+
+JobListDialog.prototype = Object.create(AbstractGuiElement.prototype);
+JobListDialog.prototype.constructor = JobListDialog;
+
+/**
+ *
+ */
+JobListDialog.prototype.createJobListDialogGui = function () {
+  var self = this;
+  var head = Functions.createElement({
+    type: "thead",
+    content: "<tr>" +
+      "<th>ID</th>" +
+      "<th>Type</th>" +
+      "<th>Status</th>" +
+      "<th>Progress</th>" +
+      "<th>Created at</th>" +
+      "<th>Finished at</th>" +
+      "<th>Priority</th>" +
+      "</tr>"
+  });
+  var body = Functions.createElement({
+    type: "tbody"
+  });
+  var tableElement = Functions.createElement({
+    type: "table",
+    className: "minerva-logs-table",
+    style: "width: 100%"
+  });
+
+  tableElement.appendChild(head);
+  tableElement.appendChild(body);
+
+  self.tableElement = tableElement;
+  self.getElement().appendChild(tableElement);
+
+  var menuRow = Functions.createElement({
+    type: "div",
+    className: "minerva-menu-row",
+    style: "display:table-row; margin:10px"
+  });
+  self.getElement().appendChild(menuRow);
+
+};
+
+function removeNull(object) {
+  if (object === null || object === undefined) {
+    return "";
+  }
+  return object;
+}
+
+function entryToRow(entry) {
+  var row = [];
+  row[0] = removeNull(entry.id);
+  row[1] = removeNull(entry.jobType);
+  row[2] = removeNull(entry.jobStatus);
+  row[3] = removeNull(entry.progress);
+  row[4] = removeNull(entry.jobStarted);
+  row[5] = removeNull(entry.jobFinished);
+  row[6] = removeNull(entry.priority);
+  return row;
+}
+
+/**
+ *
+ * @param {Object} data
+ * @param {number} data.start
+ * @param {number} data.length
+ * @param {Object} data.search
+ * @param {Object} data.draw
+ * @param {Array} data.order
+ * @param {function} callback
+ * @returns {Promise}
+ * @private
+ */
+JobListDialog.prototype._dataTableAjaxCall = function (data, callback) {
+  var self = this;
+  return self.getServerConnector().getProjectJobs({
+    start: data.start / data.length,
+    length: data.length,
+    projectId: self._projectId
+  }).then(function (jobEntries) {
+    var out = [];
+
+    for (var i = 0; i < jobEntries.content.length; i++) {
+      var entry = jobEntries.content[i];
+
+      var row = entryToRow(entry);
+      out.push(row);
+    }
+    callback({
+      draw: data.draw,
+      recordsTotal: jobEntries.totalElements,
+      recordsFiltered: jobEntries.totalElements,
+      data: out
+    });
+  });
+};
+
+JobListDialog.prototype.open = function () {
+  var self = this;
+  if (!$(self.getElement()).hasClass("ui-dialog-content")) {
+    $(self.getElement()).dialog({
+      dialogClass: 'minerva-logs-dialog',
+      title: "Job list",
+      autoOpen: false,
+      resizable: false,
+      width: Math.max(window.innerWidth / 2, window.innerWidth - 100),
+      height: Math.max(window.innerHeight / 2, window.innerHeight - 100)
+    });
+  }
+
+  $(self.getElement()).dialog("open");
+
+  if (!$.fn.DataTable.isDataTable(self.tableElement)) {
+    return new Promise(function (resolve) {
+      $(self.tableElement).dataTable({
+        serverSide: true,
+        ordering: false,
+        searching: false,
+        ajax: function (data, callback) {
+          resolve(self._dataTableAjaxCall(data, callback));
+        },
+        columns: self.getColumnsDefinition()
+      });
+    });
+  } else {
+    $(self.tableElement).dataTable().api(true).draw();
+    return Promise.resolve();
+  }
+
+};
+
+/**
+ *
+ * @returns {Array}
+ */
+JobListDialog.prototype.getColumnsDefinition = function () {
+  return [{
+    name: "id"
+  }, {
+    name: "jobType"
+  }, {
+    name: "jobStatus"
+  }, {
+    name: "progress"
+  }, {
+    name: "jobStarted"
+  }, {
+    name: "jobFinished"
+  }, {
+    name: "priority"
+  }];
+};
+
+/**
+ *
+ * @returns {Promise}
+ */
+JobListDialog.prototype.init = function () {
+  return Promise.resolve();
+};
+
+/**
+ *
+ */
+JobListDialog.prototype.destroy = function () {
+  var self = this;
+  var div = self.getElement();
+  if ($(div).hasClass("ui-dialog-content")) {
+    $(div).dialog("destroy");
+  }
+  if ($.fn.DataTable.isDataTable(self.tableElement)) {
+    $(self.tableElement).DataTable().destroy();
+  }
+};
+
+module.exports = JobListDialog;
diff --git a/frontend-js/src/main/js/gui/admin/MapsAdminPanel.js b/frontend-js/src/main/js/gui/admin/MapsAdminPanel.js
index 5179db7a0a50eb3726b5e4bec6a112ff21f9b65e..aa8a38d6adb45a1cdbbbda81f118bfb3caab449d 100644
--- a/frontend-js/src/main/js/gui/admin/MapsAdminPanel.js
+++ b/frontend-js/src/main/js/gui/admin/MapsAdminPanel.js
@@ -7,6 +7,7 @@ var AbstractAdminPanel = require('./AbstractAdminPanel');
 var AddProjectDialog = require('./AddProjectDialog');
 var EditProjectDialog = require('./EditProjectDialog');
 var LogListDialog = require('./LogListDialog');
+var JobListDialog = require('./JobListDialog');
 var PrivilegeType = require('../../map/data/PrivilegeType');
 var ConfigurationType = require('../../ConfigurationType');
 var NetworkError = require('../../NetworkError');
@@ -242,6 +243,11 @@ MapsAdminPanel.prototype._createProjectTableRow = function () {
     return self.showLogs($(button).attr("data")).catch(GuiConnector.alert);
   });
 
+  $(projectsTable).on("click", "[name='showJobs']", function () {
+    var button = this;
+    return self.showJobs($(button).attr("data")).catch(GuiConnector.alert);
+  });
+
   $(projectsTable).on("click", "[name='minerva-shared']", function () {
       var field = this;
       var projectId = $(field).attr('data');
@@ -448,6 +454,14 @@ MapsAdminPanel.prototype.projectToTableRow = function (project, row, user) {
     }
   }
 
+  if (isAdmin) {
+    icon = "<i class='fa fa-info' style='font-size:18px; padding-right:10px;color:#337ab7'></i>";
+    status += " <a name='showJobs' href='#' data='" + project.getProjectId() + "'>" + icon + "</a>";
+  } else {
+    status += icon;
+  }
+
+
   row[0] = formattedProjectId;
   var date = project.getCreationDate();
   if (date === undefined) {
@@ -768,6 +782,36 @@ MapsAdminPanel.prototype.getLogDialog = function (projectId) {
   }
 };
 
+/**
+ *
+ * @param {string} projectId
+ * @returns {Promise<JobListDialog>}
+ */
+MapsAdminPanel.prototype.getJobDialog = function (projectId) {
+  var self = this;
+  if (self._jobDialogs === undefined) {
+    self._jobDialogs = [];
+  }
+  var dialog = self._jobDialogs[projectId];
+  if (dialog === undefined) {
+    dialog = new JobListDialog({
+      element: Functions.createElement({
+        type: "div"
+      }),
+      configuration: self.getConfiguration(),
+      projectId: projectId,
+      serverConnector: self.getServerConnector(),
+      customMap: null
+    });
+    self._jobDialogs[projectId] = dialog;
+    return dialog.init().then(function () {
+      return dialog;
+    });
+  } else {
+    return Promise.resolve(dialog);
+  }
+};
+
 /**
  *
  * @param {string} id projectId
@@ -804,6 +848,21 @@ MapsAdminPanel.prototype.showLogs = function (id) {
   });
 };
 
+/**
+ *
+ * @param {string} id projectId
+ * @returns {Promise<JobListDialog>}
+ */
+MapsAdminPanel.prototype.showJobs = function (id) {
+  var self = this;
+  GuiConnector.showProcessing();
+  return self.getJobDialog(id).then(function (dialog) {
+    return dialog.open();
+  }).finally(function () {
+    GuiConnector.hideProcessing();
+  });
+};
+
 /**
  *
  * @param {string} id projectId
diff --git a/frontend-js/src/test/js/ServerConnector-mock.js b/frontend-js/src/test/js/ServerConnector-mock.js
index 1a1a1f8e86ec676c98c60fadb7e4b31855dc76b2..dd895873a048190ef7d75968c74d7a6a29b4c588 100644
--- a/frontend-js/src/test/js/ServerConnector-mock.js
+++ b/frontend-js/src/test/js/ServerConnector-mock.js
@@ -97,16 +97,39 @@ ServerConnectorMock._sendRequest = function (params) {
   });
 };
 
-ServerConnectorMock.getApiBaseUrl = function () {
-  return "./testFiles/apiCalls/";
-};
+ServerConnectorMock.Original = {
+  getApiBaseUrl: ServerConnectorMock.getApiBaseUrl,
+  getNewApiBaseUrl: ServerConnectorMock.getNewApiBaseUrl,
+  getApiUrl: ServerConnectorMock.getApiUrl
+}
 
-var originalGetApiUrl = OriginalServerConnector.getApiUrl;
+ServerConnectorMock.Mock = {
+  getApiBaseUrl: function () {
+    return "./testFiles/apiCalls/";
+  },
+  getNewApiBaseUrl: function () {
+    return "./testFiles/apiCallsNew/";
+  },
+  getApiUrl: function (paramObj) {
+    // replace '?' (or '/?') with '/'
+    // the call is done on ServerConnectorObject (so 'this' is set properly)
+    return ServerConnectorMock.Original.getApiUrl.call(this, paramObj).replace(/\/?\?/g, '/');
+  }
+}
 
-ServerConnectorMock.getApiUrl = function (paramObj) {
-  // replace '?' (or '/?') with '/'
-  // the call is done on ServerConnectorObject (so 'this' is set properly)
-  return originalGetApiUrl.call(this, paramObj).replace(/\/?\?/g, '/');
-};
+ServerConnectorMock.useMock = function () {
+  ServerConnectorMock.getApiBaseUrl = ServerConnectorMock.Mock.getApiBaseUrl;
+  ServerConnectorMock.getApiUrl = ServerConnectorMock.Mock.getApiUrl;
+  ServerConnectorMock.getNewApiBaseUrl = ServerConnectorMock.Mock.getNewApiBaseUrl;
+
+}
+
+ServerConnectorMock.useOriginal = function () {
+  ServerConnectorMock.getApiBaseUrl = ServerConnectorMock.Original.getApiBaseUrl;
+  ServerConnectorMock.getApiUrl = ServerConnectorMock.Original.getApiUrl;
+  ServerConnectorMock.getNewApiBaseUrl = ServerConnectorMock.Original.getNewApiBaseUrl;
+}
+
+ServerConnectorMock.useMock();
 
 module.exports = ServerConnectorMock;
diff --git a/frontend-js/src/test/js/ServerConnector-test.js b/frontend-js/src/test/js/ServerConnector-test.js
index e677b8bf178c0e389b66290bb5186e551fa29fbd..628c01c574a057bb3a3cf9e6b9e98fa3f42881f7 100644
--- a/frontend-js/src/test/js/ServerConnector-test.js
+++ b/frontend-js/src/test/js/ServerConnector-test.js
@@ -467,5 +467,14 @@ describe('ServerConnector', function () {
     });
   });
 
+  it('getProjectJobsUrl', function () {
+    try {
+      ServerConnector.useOriginal();
+      var url = ServerConnector.getProjectJobsUrl("test", {});
+      assert.isTrue(url.includes('new_api'), url);
+    } finally {
+      ServerConnector.useMock();
+    }
+  });
 
 });
diff --git a/model/src/main/java/lcsb/mapviewer/model/LogMarker.java b/model/src/main/java/lcsb/mapviewer/model/LogMarker.java
index 57c34da00a960c61d7d244207aead11fc34f1e56..c6f57a9d0a427c7f9ee377d8739a651f7aefea76 100644
--- a/model/src/main/java/lcsb/mapviewer/model/LogMarker.java
+++ b/model/src/main/java/lcsb/mapviewer/model/LogMarker.java
@@ -1,25 +1,25 @@
 package lcsb.mapviewer.model;
 
-import org.apache.logging.log4j.LogManager;
-import org.apache.logging.log4j.Logger;
-import org.apache.logging.log4j.Marker;
-
 import lcsb.mapviewer.common.exception.NotImplementedException;
 import lcsb.mapviewer.model.map.BioEntity;
 import lcsb.mapviewer.model.map.layout.graphics.LayerText;
 import lcsb.mapviewer.model.map.reaction.ReactionNode;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.apache.logging.log4j.Marker;
+import org.hibernate.proxy.HibernateProxy;
 
 public class LogMarker implements Marker {
 
   private static final Logger logger = LogManager.getLogger(LogMarker.class);
   /**
-   * 
+   *
    */
   private static final long serialVersionUID = 1L;
 
   public static final Marker IGNORE = new IgnoredLogMarker();
 
-  private ProjectLogEntry entry;
+  private final ProjectLogEntry entry;
 
   public LogMarker(final ProjectLogEntryType type, final BioEntity bioEntity) {
     entry = new ProjectLogEntry();
@@ -27,8 +27,8 @@ public class LogMarker implements Marker {
     if (bioEntity != null) {
       entry.setObjectIdentifier(bioEntity.getElementId());
       entry.setObjectClass(bioEntity.getClass().getSimpleName());
-      if (bioEntity.getModel() != null) {
-        entry.setMapName(bioEntity.getModel().getName());
+      if (bioEntity.getModelData() != null && !(bioEntity.getModelData() instanceof HibernateProxy)) {
+        entry.setMapName(bioEntity.getModelData().getName());
       }
     }
   }
diff --git a/model/src/main/java/lcsb/mapviewer/model/Project.java b/model/src/main/java/lcsb/mapviewer/model/Project.java
index 81e8d53a14feed3a516e0d9d922d9ddd1a38c672..300e8294fa1357153f178ea4831633e146ba96d2 100644
--- a/model/src/main/java/lcsb/mapviewer/model/Project.java
+++ b/model/src/main/java/lcsb/mapviewer/model/Project.java
@@ -375,7 +375,7 @@ public class Project implements Serializable, MinervaEntity {
 
   }
 
-  public void addLogEntries(final Set<ProjectLogEntry> createLogEntries) {
+  public void addLogEntries(final Collection<ProjectLogEntry> createLogEntries) {
     for (final ProjectLogEntry projectLogEntry : createLogEntries) {
       addLogEntry(projectLogEntry);
     }
diff --git a/model/src/main/java/lcsb/mapviewer/model/ProjectStatus.java b/model/src/main/java/lcsb/mapviewer/model/ProjectStatus.java
index b2a0c9a1eb29d46154eb1108c29a6b7afc38eacc..36e6d8858c6f94049498980f5202c56820fc3dcd 100644
--- a/model/src/main/java/lcsb/mapviewer/model/ProjectStatus.java
+++ b/model/src/main/java/lcsb/mapviewer/model/ProjectStatus.java
@@ -17,11 +17,6 @@ public enum ProjectStatus {
    */
   PARSING_DATA("Parsing data"),
 
-  /**
-   * Model is annotated by the annotation tool.
-   */
-  ANNOTATING("Annotating"),
-
   /**
    * Model is being uploaded to the database.
    */
diff --git a/model/src/main/java/lcsb/mapviewer/model/job/MinervaJob.java b/model/src/main/java/lcsb/mapviewer/model/job/MinervaJob.java
index 835cd5dd426f0cbeffe1d5b2bdb1070cf3fc3535..d878ed679186da961660f27e67c7167c4003e1f0 100644
--- a/model/src/main/java/lcsb/mapviewer/model/job/MinervaJob.java
+++ b/model/src/main/java/lcsb/mapviewer/model/job/MinervaJob.java
@@ -49,6 +49,12 @@ public class MinervaJob implements MinervaEntity {
 
   private Calendar jobFinished;
 
+  private Double progress;
+
+  private Class<?> externalObjectClass;
+
+  private Integer externalObjectId;
+
   @JsonIgnore
   private String logs;
 
@@ -128,4 +134,35 @@ public class MinervaJob implements MinervaEntity {
   public EntityType getEntityType() {
     throw new NotImplementedException();
   }
+
+  public Double getProgress() {
+    return progress;
+  }
+
+  public void setProgress(final Double progress) {
+    this.progress = progress;
+  }
+
+  public Class<?> getExternalObjectClass() {
+    return externalObjectClass;
+  }
+
+  public void setExternalObjectClass(final Class<?> externalObjectClass) {
+    this.externalObjectClass = externalObjectClass;
+  }
+
+  public Integer getExternalObjectId() {
+    return externalObjectId;
+  }
+
+  public void setExternalObjectId(final Integer externalObjectId) {
+    this.externalObjectId = externalObjectId;
+  }
+
+  public void setExternalObject(final MinervaEntity entity) {
+    if (entity != null) {
+      this.externalObjectClass = entity.getClass();
+      this.externalObjectId = entity.getId();
+    }
+  }
 }
diff --git a/model/src/main/java/lcsb/mapviewer/model/job/MinervaJobType.java b/model/src/main/java/lcsb/mapviewer/model/job/MinervaJobType.java
index 58050aaa7d03229d12da7fc44da1f1e231076974..95fb89e72483a69dac5a359d25480465e411b784 100644
--- a/model/src/main/java/lcsb/mapviewer/model/job/MinervaJobType.java
+++ b/model/src/main/java/lcsb/mapviewer/model/job/MinervaJobType.java
@@ -17,7 +17,10 @@ public enum MinervaJobType {
   CREATE_PROJECT_FAILURE,
 
   DELETE_BACKGROUND,
-  REVIVE_BACKGROUNDS;
+  REVIVE_BACKGROUNDS,
+
+  ANNOTATE_PROJECT,
+  ;
 
   private final boolean allowDuplicates;
 
diff --git a/model/src/test/java/lcsb/mapviewer/model/ProjectTest.java b/model/src/test/java/lcsb/mapviewer/model/ProjectTest.java
index 6d49dcb99ca85ada289b1d37091d118099872187..8750df2c3639733f121143a1d64c555c58e8c185 100644
--- a/model/src/test/java/lcsb/mapviewer/model/ProjectTest.java
+++ b/model/src/test/java/lcsb/mapviewer/model/ProjectTest.java
@@ -1,17 +1,5 @@
 package lcsb.mapviewer.model;
 
-import static org.junit.Assert.assertEquals;
-import static org.junit.Assert.assertNotNull;
-
-import java.util.Calendar;
-import java.util.HashSet;
-import java.util.Set;
-
-import org.apache.commons.lang3.SerializationUtils;
-import org.junit.After;
-import org.junit.Before;
-import org.junit.Test;
-
 import lcsb.mapviewer.ModelTestFunctions;
 import lcsb.mapviewer.common.Configuration;
 import lcsb.mapviewer.model.cache.UploadedFileEntry;
@@ -19,6 +7,17 @@ import lcsb.mapviewer.model.map.MiriamData;
 import lcsb.mapviewer.model.map.model.Model;
 import lcsb.mapviewer.model.map.model.ModelData;
 import lcsb.mapviewer.model.map.model.ModelFullIndexed;
+import org.apache.commons.lang3.SerializationUtils;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.util.Calendar;
+import java.util.HashSet;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
 
 public class ProjectTest extends ModelTestFunctions {
 
@@ -60,13 +59,13 @@ public class ProjectTest extends ModelTestFunctions {
   @Test
   public void testGetters() {
     MiriamData organism = new MiriamData();
-    int id = 5;
-    String projectId = "str";
-    ProjectStatus status = ProjectStatus.ANNOTATING;
-    double progress = 4.5;
+    int id = faker.number().numberBetween(1, Integer.MAX_VALUE);
+    String projectId = faker.numerify("P####");
+    ProjectStatus status = faker.options().option(ProjectStatus.class);
+    double progress = faker.number().randomDouble(2, 1, 100);
     Set<ModelData> models = new HashSet<>();
-    String directory = "dir";
-    String name = "name3";
+    String directory = faker.numerify("dir-####");
+    String name = faker.name().fullName();
     MiriamData disease = new MiriamData();
     Project project = new Project("str");
     boolean sbgn = true;
@@ -107,7 +106,7 @@ public class ProjectTest extends ModelTestFunctions {
   }
 
   @Test
-  public void testsetCreationDate() {
+  public void testSetCreationDate() {
     Project project = new Project();
     Calendar creationDate = Calendar.getInstance();
     project.setCreationDate(creationDate);
diff --git a/persist/src/main/java/lcsb/mapviewer/persist/DbUtils.java b/persist/src/main/java/lcsb/mapviewer/persist/DbUtils.java
index 8d3e13e38f43574315cdaea02021b9cc2d337abe..5863891af7caa866ccb58f11cf46f19ad3fca532 100644
--- a/persist/src/main/java/lcsb/mapviewer/persist/DbUtils.java
+++ b/persist/src/main/java/lcsb/mapviewer/persist/DbUtils.java
@@ -1,11 +1,5 @@
 package lcsb.mapviewer.persist;
 
-import java.sql.Connection;
-import java.sql.SQLException;
-import java.util.HashMap;
-import java.util.Map;
-import java.util.Observable;
-
 import org.apache.logging.log4j.LogManager;
 import org.apache.logging.log4j.Logger;
 import org.hibernate.SessionFactory;
@@ -15,6 +9,14 @@ import org.hibernate.internal.SessionFactoryImpl;
 import org.hibernate.jdbc.Work;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.sql.Connection;
+import java.sql.SQLException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Observable;
+import java.util.function.Supplier;
 
 /**
  * This class contains a set of methods that allow to manage hibernate sessions
@@ -26,9 +28,8 @@ import org.springframework.stereotype.Service;
  * </ul>
  * Class extends {@link Observable}. Whenever session is created or closed
  * observers are notified.
- * 
+ *
  * @author Piotr Gawron
- * 
  */
 @SuppressWarnings("deprecation")
 @Service
@@ -37,7 +38,7 @@ public class DbUtils extends Observable {
   /**
    * Default class logger.
    */
-  private static Logger logger = LogManager.getLogger();
+  private static final Logger logger = LogManager.getLogger();
 
   /**
    * This determines if every add/update/delete operation should be flushed (for
@@ -49,7 +50,7 @@ public class DbUtils extends Observable {
    * might be some problems with long transactions.</li>
    * </ol>
    */
-  private static Map<Long, Boolean> autoFlushForThread = new HashMap<>();
+  private static final Map<Long, Boolean> autoFlushForThread = new HashMap<>();
 
   /**
    * Hibernate session factory.
@@ -70,8 +71,7 @@ public class DbUtils extends Observable {
   }
 
   /**
-   * @param sessionFactory
-   *          the sessionFactory to set
+   * @param sessionFactory the sessionFactory to set
    * @see #sessionFactory
    */
   public void setSessionFactory(final SessionFactory sessionFactory) {
@@ -81,9 +81,9 @@ public class DbUtils extends Observable {
   /**
    * Returns info if the flush is automatically done or not in current
    * {@link Thread}.
-   * 
+   *
    * @return <code>true</code> if flush is automatically done,
-   *         <code>false</code> otherwise
+   * <code>false</code> otherwise
    * @see #autoFlushForThread
    */
   public boolean isAutoFlush() {
@@ -104,9 +104,8 @@ public class DbUtils extends Observable {
 
   /**
    * Set autoFlush for current {@link Thread}.
-   * 
-   * @param autoFlush
-   *          the new autoflush value
+   *
+   * @param autoFlush the new autoflush value
    * @see #autoFlushForThread
    */
   public void setAutoFlush(final boolean autoFlush) {
@@ -143,4 +142,15 @@ public class DbUtils extends Observable {
   public void dumpDbToFile(final String filename) {
     sessionFactory.getCurrentSession().createNativeQuery("SCRIPT '" + filename + "'").executeUpdate();
   }
+
+  @Transactional
+  public void callInTransaction(final Supplier<Void> function) {
+    function.get();
+  }
+
+  @Transactional
+  public <T> T callInTransactionWithResult(final Supplier<T> function) {
+    return function.get();
+  }
+
 }
diff --git a/persist/src/main/java/lcsb/mapviewer/persist/dao/BaseDao.java b/persist/src/main/java/lcsb/mapviewer/persist/dao/BaseDao.java
index b27d5bafdc5cb28290ae9fc44ea88a6a81ae5430..ac96b749dde60d3b5d6e1d0eef1bbe8ef79da870 100644
--- a/persist/src/main/java/lcsb/mapviewer/persist/dao/BaseDao.java
+++ b/persist/src/main/java/lcsb/mapviewer/persist/dao/BaseDao.java
@@ -360,6 +360,11 @@ public abstract class BaseDao<T extends MinervaEntity, S extends MinervaEntityPr
     return new PageImpl<>(sublist, pageable, count);
   }
 
+  public List<T> getAll(final Map<S, Object> filterOptions) {
+    return getAll(filterOptions, Pageable.unpaged()).getContent();
+  }
+
+
   protected List<Order> getOrder(final Sort sort, final CriteriaBuilder criteriaBuilder, final Root<T> root) {
     for (Iterator<Sort.Order> iterator = sort.iterator(); iterator.hasNext(); ) {
       Sort.Order order = iterator.next();
diff --git a/persist/src/main/java/lcsb/mapviewer/persist/dao/MinervaJobDao.java b/persist/src/main/java/lcsb/mapviewer/persist/dao/MinervaJobDao.java
index 18f50b8e8d87d91d534922787254be6812794357..c9fd86bb635549ffa93bffbf012f5abf9372eb4d 100644
--- a/persist/src/main/java/lcsb/mapviewer/persist/dao/MinervaJobDao.java
+++ b/persist/src/main/java/lcsb/mapviewer/persist/dao/MinervaJobDao.java
@@ -1,5 +1,6 @@
 package lcsb.mapviewer.persist.dao;
 
+import lcsb.mapviewer.common.exception.InvalidArgumentException;
 import lcsb.mapviewer.model.job.MinervaJob;
 import lcsb.mapviewer.model.job.MinervaJobStatus;
 import lcsb.mapviewer.model.job.MinervaJobType;
@@ -7,14 +8,17 @@ import org.springframework.stereotype.Repository;
 
 import javax.persistence.criteria.CriteriaBuilder;
 import javax.persistence.criteria.CriteriaQuery;
+import javax.persistence.criteria.Predicate;
 import javax.persistence.criteria.Root;
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Calendar;
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
 @Repository
-public class MinervaJobDao extends BaseDao<MinervaJob, MinervaEntityProperty<MinervaJob>> {
+public class MinervaJobDao extends BaseDao<MinervaJob, MinervaJobProperty> {
 
   public MinervaJobDao() {
     super(MinervaJob.class);
@@ -88,4 +92,35 @@ public class MinervaJobDao extends BaseDao<MinervaJob, MinervaEntityProperty<Min
         .setParameter("job_type", jobType)
         .list();
   }
+
+  @Override
+  protected Predicate createPredicate(final Map<MinervaJobProperty, Object> filterOptions, final Root<MinervaJob> root) {
+    CriteriaBuilder builder = getSession().getCriteriaBuilder();
+    List<Predicate> predicates = new ArrayList<>();
+
+    for (MinervaJobProperty key : filterOptions.keySet()) {
+      if (key.equals(MinervaJobProperty.EXTERNAL_OBJECT_CLASS)) {
+        Collection<Class<?>> values = (Collection<Class<?>>) filterOptions.get(key);
+
+        final CriteriaBuilder.In<Class<?>> predicate = builder.in(root.get("externalObjectClass"));
+        for (final Class<?> object : values) {
+          predicate.value(object);
+        }
+        predicates.add(predicate);
+      } else if (key.equals(MinervaJobProperty.EXTERNAL_OBJECT_ID)) {
+        Collection<Integer> values = (Collection<Integer>) filterOptions.get(key);
+
+        final CriteriaBuilder.In<Integer> predicate = builder.in(root.get("externalObjectId"));
+        for (final Integer object : values) {
+          predicate.value(object);
+        }
+        predicates.add(predicate);
+      } else {
+        throw new InvalidArgumentException("Unknown property: " + key);
+      }
+    }
+    Predicate predicate = builder.and(predicates.toArray(new Predicate[0]));
+    return predicate;
+
+  }
 }
diff --git a/persist/src/main/java/lcsb/mapviewer/persist/dao/MinervaJobProperty.java b/persist/src/main/java/lcsb/mapviewer/persist/dao/MinervaJobProperty.java
new file mode 100644
index 0000000000000000000000000000000000000000..4f7bbff5f593cc50b46e74145b95b293c2e75688
--- /dev/null
+++ b/persist/src/main/java/lcsb/mapviewer/persist/dao/MinervaJobProperty.java
@@ -0,0 +1,8 @@
+package lcsb.mapviewer.persist.dao;
+
+import lcsb.mapviewer.model.job.MinervaJob;
+
+public enum MinervaJobProperty implements MinervaEntityProperty<MinervaJob> {
+  EXTERNAL_OBJECT_CLASS,
+  EXTERNAL_OBJECT_ID,
+}
diff --git a/persist/src/main/resources/db/migration/hsql/19.0.0~alpha.0/V19.0.0.20250227.2__add_progress_to_minerva_job.sql b/persist/src/main/resources/db/migration/hsql/19.0.0~alpha.0/V19.0.0.20250227.2__add_progress_to_minerva_job.sql
new file mode 100644
index 0000000000000000000000000000000000000000..eb923d432a7c6c918ea92789ec1f3bd6848bdb3a
--- /dev/null
+++ b/persist/src/main/resources/db/migration/hsql/19.0.0~alpha.0/V19.0.0.20250227.2__add_progress_to_minerva_job.sql
@@ -0,0 +1,2 @@
+alter table minerva_job_table
+    add column progress double precision;
\ No newline at end of file
diff --git a/persist/src/main/resources/db/migration/hsql/19.0.0~alpha.0/V19.0.0.20250227.3__add_object_connection_to_minerva_job.sql b/persist/src/main/resources/db/migration/hsql/19.0.0~alpha.0/V19.0.0.20250227.3__add_object_connection_to_minerva_job.sql
new file mode 100644
index 0000000000000000000000000000000000000000..4a735d4e773c6732a663c02cbe20ba56fcd20c80
--- /dev/null
+++ b/persist/src/main/resources/db/migration/hsql/19.0.0~alpha.0/V19.0.0.20250227.3__add_object_connection_to_minerva_job.sql
@@ -0,0 +1,5 @@
+alter table minerva_job_table
+    add column external_object_id integer;
+
+alter table minerva_job_table
+    add column external_object_class character varying(512);
diff --git a/persist/src/main/resources/db/migration/hsql/19.0.0~alpha.0/V19.0.0.20250227__remove_annotating_project_status.sql b/persist/src/main/resources/db/migration/hsql/19.0.0~alpha.0/V19.0.0.20250227__remove_annotating_project_status.sql
new file mode 100644
index 0000000000000000000000000000000000000000..e6de86f9f4de92fc8656ddcc9996aa6d227503b8
--- /dev/null
+++ b/persist/src/main/resources/db/migration/hsql/19.0.0~alpha.0/V19.0.0.20250227__remove_annotating_project_status.sql
@@ -0,0 +1,3 @@
+update project_table
+set status ='UNKNOWN'
+where status in ('ANNOTATING');
\ No newline at end of file
diff --git a/persist/src/main/resources/db/migration/postgres/19.0.0~alpha.0/V19.0.0.20250227.2__add_progress_to_minerva_job.sql b/persist/src/main/resources/db/migration/postgres/19.0.0~alpha.0/V19.0.0.20250227.2__add_progress_to_minerva_job.sql
new file mode 100644
index 0000000000000000000000000000000000000000..eb923d432a7c6c918ea92789ec1f3bd6848bdb3a
--- /dev/null
+++ b/persist/src/main/resources/db/migration/postgres/19.0.0~alpha.0/V19.0.0.20250227.2__add_progress_to_minerva_job.sql
@@ -0,0 +1,2 @@
+alter table minerva_job_table
+    add column progress double precision;
\ No newline at end of file
diff --git a/persist/src/main/resources/db/migration/postgres/19.0.0~alpha.0/V19.0.0.20250227.3__add_object_connection_to_minerva_job.sql b/persist/src/main/resources/db/migration/postgres/19.0.0~alpha.0/V19.0.0.20250227.3__add_object_connection_to_minerva_job.sql
new file mode 100644
index 0000000000000000000000000000000000000000..4a735d4e773c6732a663c02cbe20ba56fcd20c80
--- /dev/null
+++ b/persist/src/main/resources/db/migration/postgres/19.0.0~alpha.0/V19.0.0.20250227.3__add_object_connection_to_minerva_job.sql
@@ -0,0 +1,5 @@
+alter table minerva_job_table
+    add column external_object_id integer;
+
+alter table minerva_job_table
+    add column external_object_class character varying(512);
diff --git a/persist/src/main/resources/db/migration/postgres/19.0.0~alpha.0/V19.0.0.20250227__remove_annotating_project_status.sql b/persist/src/main/resources/db/migration/postgres/19.0.0~alpha.0/V19.0.0.20250227__remove_annotating_project_status.sql
new file mode 100644
index 0000000000000000000000000000000000000000..e6de86f9f4de92fc8656ddcc9996aa6d227503b8
--- /dev/null
+++ b/persist/src/main/resources/db/migration/postgres/19.0.0~alpha.0/V19.0.0.20250227__remove_annotating_project_status.sql
@@ -0,0 +1,3 @@
+update project_table
+set status ='UNKNOWN'
+where status in ('ANNOTATING');
\ No newline at end of file
diff --git a/persist/src/test/java/lcsb/mapviewer/persist/utils/TransactionalElementDao.java b/persist/src/test/java/lcsb/mapviewer/persist/utils/TransactionalElementDao.java
index d173897f9c4c4387e0eea6dcf58305a9aabcf1a8..ca5a1a88f05f6c2b0302ba1926f1a58a05783fd0 100644
--- a/persist/src/test/java/lcsb/mapviewer/persist/utils/TransactionalElementDao.java
+++ b/persist/src/test/java/lcsb/mapviewer/persist/utils/TransactionalElementDao.java
@@ -1,16 +1,15 @@
 package lcsb.mapviewer.persist.utils;
 
-import java.util.Map;
-
+import lcsb.mapviewer.model.map.species.Element;
+import lcsb.mapviewer.persist.dao.map.species.ElementDao;
+import lcsb.mapviewer.persist.dao.map.species.ElementProperty;
 import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.data.domain.Page;
 import org.springframework.data.domain.PageRequest;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 
-import lcsb.mapviewer.model.map.species.Element;
-import lcsb.mapviewer.persist.dao.map.species.ElementDao;
-import lcsb.mapviewer.persist.dao.map.species.ElementProperty;
+import java.util.Map;
 
 @Component
 @Transactional
diff --git a/rest-api/src/main/java/lcsb/mapviewer/api/users/UserController.java b/rest-api/src/main/java/lcsb/mapviewer/api/users/UserController.java
index 08d1652c7ce7ff605273ff079e4c22f38757bccf..7cbaf4f9eb6436467c4f75d3eef07b29fb771562 100644
--- a/rest-api/src/main/java/lcsb/mapviewer/api/users/UserController.java
+++ b/rest-api/src/main/java/lcsb/mapviewer/api/users/UserController.java
@@ -21,8 +21,8 @@ import lcsb.mapviewer.modelutils.serializer.model.security.PrivilegeKeyDeseriali
 import lcsb.mapviewer.services.InvalidTokenException;
 import lcsb.mapviewer.services.ObjectExistsException;
 import lcsb.mapviewer.services.ObjectNotFoundException;
+import lcsb.mapviewer.services.interfaces.IAnnotationService;
 import lcsb.mapviewer.services.interfaces.IConfigurationService;
-import lcsb.mapviewer.services.interfaces.IProjectService;
 import lcsb.mapviewer.services.interfaces.IUserService;
 import lcsb.mapviewer.services.utils.EmailException;
 import lcsb.mapviewer.services.utils.EmailSender;
@@ -82,7 +82,7 @@ import java.util.stream.Collectors;
 public class UserController extends BaseController {
 
   private final IUserService userService;
-  private final IProjectService projectService;
+  private final IAnnotationService annotationService;
   private final IConfigurationService configurationService;
   private final PasswordEncoder passwordEncoder;
   private final EmailSender emailSender;
@@ -94,14 +94,14 @@ public class UserController extends BaseController {
                         final PasswordEncoder passwordEncoder,
                         final EmailSender emailSender,
                         final IConfigurationService configurationService,
-                        final IProjectService projectService,
+                        final IAnnotationService annotationService,
                         final SessionRegistry sessionRegistry) {
     this.userService = userService;
     this.passwordEncoder = passwordEncoder;
     this.emailSender = emailSender;
-    this.projectService = projectService;
     this.configurationService = configurationService;
     this.sessionRegistry = sessionRegistry;
+    this.annotationService = annotationService;
   }
 
   /**
@@ -129,7 +129,7 @@ public class UserController extends BaseController {
       throw new ObjectNotFoundException("User doesn't exist");
     }
     if (user.getAnnotationSchema() == null) {
-      user.setAnnotationSchema(projectService.prepareUserAnnotationSchema(user, true));
+      user.setAnnotationSchema(annotationService.prepareUserAnnotationSchema(user, true));
     }
     Boolean ldapAvailable = false;
     if (columnSet.contains("ldapAccountAvailable")) {
@@ -221,7 +221,7 @@ public class UserController extends BaseController {
         throw new ObjectNotFoundException("User doesn't exist");
       }
 
-      final UserAnnotationSchema schema = projectService.prepareUserAnnotationSchema(modifiedUser, true);
+      final UserAnnotationSchema schema = annotationService.prepareUserAnnotationSchema(modifiedUser, true);
 
       if (body.getPreferences().getValidateMiriamTypes() != null) {
         schema.setValidateMiriamTypes(body.getPreferences().getValidateMiriamTypes());
@@ -305,7 +305,7 @@ public class UserController extends BaseController {
       data.add(new UserDTO(user, ldapAvailability.get(user.getLogin()), getLastActive(user)));
     }
     data = data.stream()
-        .sorted(Comparator.comparing(user -> user.getLogin(), Comparator.reverseOrder()))
+        .sorted(Comparator.comparing(User::getLogin, Comparator.reverseOrder()))
         .collect(Collectors.toList());
 
     return createResponseWithColumns(columns, data);
diff --git a/service/src/main/java/lcsb/mapviewer/services/impl/AnnotationService.java b/service/src/main/java/lcsb/mapviewer/services/impl/AnnotationService.java
new file mode 100644
index 0000000000000000000000000000000000000000..31c6bdc94320c7f3b48ddeaab8e119ac62f31fbb
--- /dev/null
+++ b/service/src/main/java/lcsb/mapviewer/services/impl/AnnotationService.java
@@ -0,0 +1,132 @@
+package lcsb.mapviewer.services.impl;
+
+import lcsb.mapviewer.annotation.services.ModelAnnotator;
+import lcsb.mapviewer.common.exception.NotImplementedException;
+import lcsb.mapviewer.model.Project;
+import lcsb.mapviewer.model.job.MinervaJob;
+import lcsb.mapviewer.model.job.MinervaJobType;
+import lcsb.mapviewer.model.user.User;
+import lcsb.mapviewer.model.user.UserAnnotationSchema;
+import lcsb.mapviewer.model.user.UserClassAnnotators;
+import lcsb.mapviewer.modelutils.map.ClassTreeNode;
+import lcsb.mapviewer.modelutils.map.ElementUtils;
+import lcsb.mapviewer.services.interfaces.IAnnotationService;
+import lcsb.mapviewer.services.interfaces.IMinervaJobService;
+import lcsb.mapviewer.services.interfaces.IProjectService;
+import lcsb.mapviewer.services.interfaces.IUserService;
+import lcsb.mapviewer.services.jobs.AnnotateProjectMinervaJob;
+import lcsb.mapviewer.services.search.drug.IDrugService;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import javax.annotation.PostConstruct;
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.function.Consumer;
+
+@Service
+public class AnnotationService implements IAnnotationService {
+
+  private final Logger logger = LogManager.getLogger();
+
+  private final ModelAnnotator modelAnnotator;
+
+  private final IUserService userService;
+
+  private final IMinervaJobService minervaJobService;
+
+  @Autowired
+  private IAnnotationService self;
+
+  private final IProjectService projectService;
+  private final IDrugService drugService;
+
+  @Autowired
+  public AnnotationService(final IMinervaJobService minervaJobService,
+                           final ModelAnnotator modelAnnotator,
+                           final IUserService userService,
+                           final IProjectService projectService,
+                           final IDrugService drugService) {
+    this.minervaJobService = minervaJobService;
+    this.modelAnnotator = modelAnnotator;
+    this.userService = userService;
+    this.projectService = projectService;
+    this.drugService = drugService;
+  }
+
+  @PostConstruct
+  public void init() {
+    minervaJobService.registerExecutor(MinervaJobType.ANNOTATE_PROJECT, self);
+  }
+
+
+  @Override
+  public void execute(final MinervaJob job) throws Exception {
+    if (job.getJobType() == MinervaJobType.ANNOTATE_PROJECT) {
+      AnnotateProjectMinervaJob parameters = new AnnotateProjectMinervaJob(job.getJobParameters());
+      self.handleAnnotateProjectJob(parameters, (final Double progress) -> minervaJobService.updateJobProgress(job, progress));
+
+      Project project = projectService.getProjectByProjectId(parameters.getProjectId());
+
+      projectService.submitRefreshChemicalInfoJobs(parameters.getProjectId());
+      drugService.submitRefreshChemblSuggestedQueryList(project.getOrganism(), project);
+      drugService.submitRefreshDrugBankSuggestedQueryList(project.getOrganism(), project);
+
+    } else {
+      throw new NotImplementedException(job.getJobType() + " not implemented");
+    }
+  }
+
+
+  /**
+   * Retrieves (or creates) annotation schema for a given user.
+   *
+   * @return {@link UserAnnotationSchema} for {@link User}
+   */
+  @Override
+  public UserAnnotationSchema prepareUserAnnotationSchema(final User user, final boolean initializeLazy) {
+    UserAnnotationSchema annotationSchema = null;
+    if (user != null) {
+      annotationSchema = userService.getById(user.getId()).getAnnotationSchema();
+      if (annotationSchema != null && annotationSchema.getClassAnnotators().isEmpty()) {
+        for (final UserClassAnnotators uca : modelAnnotator.createDefaultAnnotatorSchema().getClassAnnotators()) {
+          annotationSchema.addClassAnnotator(uca);
+        }
+      }
+    }
+    if (annotationSchema == null) {
+      annotationSchema = modelAnnotator.createDefaultAnnotatorSchema();
+
+      final ElementUtils elementUtils = new ElementUtils();
+
+      final ClassTreeNode top = elementUtils.getAnnotatedElementClassTree();
+
+      final Queue<ClassTreeNode> nodes = new LinkedList<>();
+      nodes.add(top);
+
+      while (!nodes.isEmpty()) {
+        final ClassTreeNode element = nodes.poll();
+        annotationSchema.addClassAnnotator(new UserClassAnnotators(element.getClazz(),
+            modelAnnotator.getDefaultAnnotators(element.getClazz())));
+        nodes.addAll(element.getChildren());
+      }
+      if (user != null) {
+        final User dbUser = userService.getById(user.getId());
+        dbUser.setAnnotationSchema(annotationSchema);
+        userService.update(dbUser);
+      }
+    }
+    if (user != null) {
+      return userService.getUserByLogin(user.getLogin(), initializeLazy).getAnnotationSchema();
+    }
+    return annotationSchema;
+  }
+
+  @Override
+  public void handleAnnotateProjectJob(final AnnotateProjectMinervaJob parameters, final Consumer<Double> updateProgress) {
+    Project project = projectService.getProjectByProjectId(parameters.getProjectId());
+    modelAnnotator.performAnnotations(project, updateProgress::accept, parameters.getSchema());
+  }
+}
diff --git a/service/src/main/java/lcsb/mapviewer/services/impl/MinervaJobService.java b/service/src/main/java/lcsb/mapviewer/services/impl/MinervaJobService.java
index 21ea237cb907f290a1f32544047eddbf7cef2677..f70ac17dbd094d0d531901469ddb489543503b9a 100644
--- a/service/src/main/java/lcsb/mapviewer/services/impl/MinervaJobService.java
+++ b/service/src/main/java/lcsb/mapviewer/services/impl/MinervaJobService.java
@@ -3,6 +3,7 @@ package lcsb.mapviewer.services.impl;
 import lcsb.mapviewer.annotation.cache.CacheQueryMinervaJob;
 import lcsb.mapviewer.annotation.cache.GeneralCacheInterface;
 import lcsb.mapviewer.common.MinervaConfigurationHolder;
+import lcsb.mapviewer.common.exception.NotImplementedException;
 import lcsb.mapviewer.model.cache.CacheQuery;
 import lcsb.mapviewer.model.cache.CacheType;
 import lcsb.mapviewer.model.job.MinervaJob;
@@ -12,6 +13,7 @@ import lcsb.mapviewer.model.job.MinervaJobStatus;
 import lcsb.mapviewer.model.job.MinervaJobType;
 import lcsb.mapviewer.persist.CustomDatabasePopulator;
 import lcsb.mapviewer.persist.dao.MinervaJobDao;
+import lcsb.mapviewer.persist.dao.MinervaJobProperty;
 import lcsb.mapviewer.persist.dao.cache.CacheTypeDao;
 import lcsb.mapviewer.services.interfaces.IMinervaJobService;
 import org.apache.commons.lang3.exception.ExceptionUtils;
@@ -22,6 +24,8 @@ import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.boot.SpringApplication;
 import org.springframework.context.ApplicationContext;
 import org.springframework.context.ApplicationContextAware;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
 import org.springframework.jdbc.datasource.init.ScriptException;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -135,7 +139,7 @@ public class MinervaJobService implements IMinervaJobService, ApplicationContext
             logger.error("QUEUE interrupted", e);
             break;
           }
-          logger.info("Recovering attempt: " + errorIssues);
+          logger.info("Recovering attempt: {}", errorIssues);
         }
       }
       if (MinervaJobService.shutdownOnFailure) {
@@ -164,7 +168,6 @@ public class MinervaJobService implements IMinervaJobService, ApplicationContext
   @Override
   public void setApplicationContext(final ApplicationContext context) throws BeansException {
     this.context = context;
-
   }
 
   @Override
@@ -248,6 +251,12 @@ public class MinervaJobService implements IMinervaJobService, ApplicationContext
     return minervaJobDao.getCount(status);
   }
 
+  @Override
+  public long getCount(final Map<MinervaJobProperty, Object> filterOptions) {
+    return minervaJobDao.getCount(filterOptions);
+  }
+
+
   @Override
   public void registerExecutor(final MinervaJobType jobType, final MinervaJobExecutor executor) {
     executors.put(jobType, executor);
@@ -303,6 +312,17 @@ public class MinervaJobService implements IMinervaJobService, ApplicationContext
     MinervaJobService.queueEnabled = false;
   }
 
+  @Override
+  public void updateJobProgress(final MinervaJob job, final Double progress) {
+    MinervaJob dbJob = self.getJobById(job.getId());
+    if (dbJob != null) {
+      dbJob.setProgress(progress);
+      minervaJobDao.update(dbJob);
+    } else {
+      logger.warn("Job not found: {}", job.getId());
+    }
+  }
+
   private Callable<Object> createJobExecutionWrapper(final MinervaJob job) {
     return new Callable<Object>() {
       @Override
@@ -312,10 +332,12 @@ public class MinervaJobService implements IMinervaJobService, ApplicationContext
 
           MinervaJobExecutor executor = getExecutor(job.getJobType());
           if (executor == null) {
-            logger.error("Cannot find executor for job: " + job.getJobType() + " (job id: " + job.getId() + ")");
-            job.setJobStatus(MinervaJobStatus.FAILED);
-            job.setLogs("Cannot find executor for job: " + job.getJobType() + " (job id: " + job.getId() + ")");
-            return self.updateJob(job);
+            logger.error("Cannot find executor for job: {} (job id: {})", job.getJobType(), job.getId());
+            
+            MinervaJob dbJob = self.getJobById(job.getId());
+            dbJob.setJobStatus(MinervaJobStatus.FAILED);
+            dbJob.setLogs("Cannot find executor for job: " + dbJob.getJobType() + " (job id: " + dbJob.getId() + ")");
+            return self.updateJob(dbJob);
           }
           executor.execute(job);
 
@@ -356,22 +378,49 @@ public class MinervaJobService implements IMinervaJobService, ApplicationContext
   }
 
   private void startJob(final MinervaJob job) {
-    logger.debug("Starting job: " + job.getId() + ", " + job.getJobType() + ", priority: " + job.getPriority() + ", " + job.getJobParameters());
-    job.setJobStatus(MinervaJobStatus.RUNNING);
-    self.updateJob(job);
+    logger.debug("Starting job: {}, {}, priority: {}, {}", job.getId(), job.getJobType(), job.getPriority(), job.getJobParameters());
+    MinervaJob dbJob = self.getJobById(job.getId());
+    dbJob.setJobStatus(MinervaJobStatus.RUNNING);
+    self.updateJob(dbJob);
   }
 
   private MinervaJob finishJob(final MinervaJob job) {
-    logger.debug("Job finished: " + job.getId());
-    job.setJobStatus(MinervaJobStatus.DONE);
-    return self.updateJob(job);
+    logger.debug("Job finished: {}", job.getId());
+    MinervaJob dbJob = self.getJobById(job.getId());
+    dbJob.setJobStatus(MinervaJobStatus.DONE);
+    return self.updateJob(dbJob);
   }
 
   private MinervaJob finishJobWithProblems(final MinervaJob job, final Throwable e) {
-    logger.error("Problem with executing job: " + job.getJobType() + " (job id: " + job.getId() + ")", e);
-    job.setJobStatus(MinervaJobStatus.FAILED);
-    job.setLogs(ExceptionUtils.getStackTrace(e));
-    return self.updateJob(job);
+    logger.error("Problem with executing job: {} (job id: {})", job.getJobType(), job.getId(), e);
+    MinervaJob dbJob = self.getJobById(job.getId());
+    dbJob.setJobStatus(MinervaJobStatus.FAILED);
+    dbJob.setLogs(ExceptionUtils.getStackTrace(e));
+    return self.updateJob(dbJob);
+  }
+
+  @Override
+  public Page<MinervaJob> getAll(final Map<MinervaJobProperty, Object> filter, final Pageable pageable) {
+    return minervaJobDao.getAll(filter, pageable);
   }
 
+  @Override
+  public MinervaJob getById(final int id) {
+    return minervaJobDao.getById(id);
+  }
+
+  @Override
+  public void update(final MinervaJob object) {
+    self.updateJob(object);
+  }
+
+  @Override
+  public void remove(final MinervaJob object) {
+    throw new NotImplementedException();
+  }
+
+  @Override
+  public void add(final MinervaJob object) {
+    self.addJob(object);
+  }
 }
diff --git a/service/src/main/java/lcsb/mapviewer/services/impl/ProjectService.java b/service/src/main/java/lcsb/mapviewer/services/impl/ProjectService.java
index 813e4f2b662119c657f78dc43ae494c8470bd26a..11e01fb110b161a1daef817dfa9260062bbddf80 100644
--- a/service/src/main/java/lcsb/mapviewer/services/impl/ProjectService.java
+++ b/service/src/main/java/lcsb/mapviewer/services/impl/ProjectService.java
@@ -1,7 +1,6 @@
 package lcsb.mapviewer.services.impl;
 
 import lcsb.mapviewer.annotation.services.MeSHParser;
-import lcsb.mapviewer.annotation.services.ModelAnnotator;
 import lcsb.mapviewer.annotation.services.TaxonomyBackend;
 import lcsb.mapviewer.annotation.services.TaxonomySearchException;
 import lcsb.mapviewer.commands.ClearColorModelCommand;
@@ -12,7 +11,6 @@ import lcsb.mapviewer.common.IProgressUpdater;
 import lcsb.mapviewer.common.MinervaConfigurationHolder;
 import lcsb.mapviewer.common.MinervaLoggerAppender;
 import lcsb.mapviewer.common.Pair;
-import lcsb.mapviewer.common.exception.InvalidArgumentException;
 import lcsb.mapviewer.common.exception.NotImplementedException;
 import lcsb.mapviewer.converter.ColorSchemaReader;
 import lcsb.mapviewer.converter.ComplexZipConverter;
@@ -54,25 +52,25 @@ import lcsb.mapviewer.model.overlay.InvalidDataOverlayException;
 import lcsb.mapviewer.model.user.ConfigurationElementType;
 import lcsb.mapviewer.model.user.User;
 import lcsb.mapviewer.model.user.UserAnnotationSchema;
-import lcsb.mapviewer.model.user.UserClassAnnotators;
-import lcsb.mapviewer.modelutils.map.ClassTreeNode;
-import lcsb.mapviewer.modelutils.map.ElementUtils;
 import lcsb.mapviewer.persist.DbUtils;
 import lcsb.mapviewer.persist.ObjectValidator;
 import lcsb.mapviewer.persist.dao.ProjectDao;
 import lcsb.mapviewer.persist.dao.ProjectLogEntryDao;
 import lcsb.mapviewer.persist.dao.ProjectLogEntryProperty;
 import lcsb.mapviewer.persist.dao.ProjectProperty;
+import lcsb.mapviewer.persist.dao.graphics.LayerProperty;
 import lcsb.mapviewer.persist.dao.map.ModelProperty;
-import lcsb.mapviewer.persist.dao.user.UserDao;
+import lcsb.mapviewer.persist.dao.user.UserAnnotationSchemaDao;
 import lcsb.mapviewer.services.interfaces.ICommentService;
 import lcsb.mapviewer.services.interfaces.IConfigurationService;
 import lcsb.mapviewer.services.interfaces.IFileService;
+import lcsb.mapviewer.services.interfaces.ILayerService;
 import lcsb.mapviewer.services.interfaces.IMinervaJobService;
 import lcsb.mapviewer.services.interfaces.IModelService;
 import lcsb.mapviewer.services.interfaces.IProjectBackgroundService;
 import lcsb.mapviewer.services.interfaces.IProjectService;
 import lcsb.mapviewer.services.interfaces.IUserService;
+import lcsb.mapviewer.services.jobs.AnnotateProjectMinervaJob;
 import lcsb.mapviewer.services.jobs.CreateProjectFailureMinervaJob;
 import lcsb.mapviewer.services.jobs.CreateProjectMinervaJob;
 import lcsb.mapviewer.services.jobs.DeleteBackgroundMinervaJob;
@@ -82,8 +80,6 @@ import lcsb.mapviewer.services.jobs.RefreshChemicalSuggestedQueryListMinervaJob;
 import lcsb.mapviewer.services.jobs.RefreshDrugInfoMinervaJob;
 import lcsb.mapviewer.services.jobs.RefreshMiriamInfoMinervaJob;
 import lcsb.mapviewer.services.jobs.ReviveBackgroundsMinervaJob;
-import lcsb.mapviewer.services.search.chemical.IChemicalService;
-import lcsb.mapviewer.services.search.drug.IDrugService;
 import lcsb.mapviewer.services.utils.CreateProjectParams;
 import lcsb.mapviewer.services.utils.EmailException;
 import lcsb.mapviewer.services.utils.EmailSender;
@@ -99,7 +95,6 @@ import org.springframework.data.domain.Pageable;
 import org.springframework.stereotype.Service;
 
 import javax.annotation.PostConstruct;
-import javax.mail.MessagingException;
 import javax.persistence.PersistenceException;
 import java.io.ByteArrayInputStream;
 import java.io.File;
@@ -109,11 +104,9 @@ import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
-import java.util.Queue;
 import java.util.Set;
 
 /**
@@ -140,23 +133,17 @@ public class ProjectService implements IProjectService {
   /**
    * Data access object for projects.
    */
-  private ProjectDao projectDao;
+  private final ProjectDao projectDao;
 
   /**
    * Data access object for models.
    */
   private final ProjectLogEntryDao projectLogEntryDao;
 
-  /**
-   * Data access object for users.
-   */
-  private final UserDao userDao;
-
-  /**
-   * Service that allows to access and manage models.
-   */
   private final IModelService modelService;
 
+  private final ILayerService layerService;
+
   /**
    * Service that allows to access and manage comments.
    */
@@ -172,24 +159,9 @@ public class ProjectService implements IProjectService {
    */
   private final IUserService userService;
 
-  /**
-   * Services that access data about chemicals.
-   */
-  private final IChemicalService chemicalService;
-
-  /**
-   * Services that access data about drugs.
-   */
-  private final IDrugService drugService;
-
   @Autowired
   private IFileService fileService;
 
-  /**
-   * Module that allows to annotate maps.
-   */
-  private final ModelAnnotator modelAnnotator;
-
   private final IProjectBackgroundService projectBackgroundService;
 
   private final IMinervaJobService minervaJobService;
@@ -217,8 +189,10 @@ public class ProjectService implements IProjectService {
 
   private final MinervaConfigurationHolder minervaConfigurationHolder;
 
+  private final UserAnnotationSchemaDao userAnnotationSchemaDao;
+
   /**
-   * Class that helps to generate images for google maps API.
+   * Class that helps to generate images for frontend.
    */
   private final MapGenerator generator = new MapGenerator();
 
@@ -226,35 +200,31 @@ public class ProjectService implements IProjectService {
   public ProjectService(final MinervaConfigurationHolder minervaConfigurationHolder,
                         final ProjectDao projectDao,
                         final ProjectLogEntryDao projectLogEntryDao,
-                        final UserDao userDao,
                         final IModelService modelService,
+                        final ILayerService layerService,
                         final ICommentService commentService,
                         final IConfigurationService configurationService,
                         final IUserService userService,
-                        final IChemicalService chemicalService,
-                        final IDrugService drugService,
-                        final ModelAnnotator modelAnnotator,
                         final DbUtils dbUtils,
                         final MeSHParser meshParser,
                         final TaxonomyBackend taxonomyBackend,
                         final IProjectBackgroundService projectBackgroundService,
-                        final IMinervaJobService minervaJobService) {
+                        final IMinervaJobService minervaJobService,
+                        final UserAnnotationSchemaDao userAnnotationSchemaDao) {
     this.minervaConfigurationHolder = minervaConfigurationHolder;
     this.projectDao = projectDao;
     this.projectLogEntryDao = projectLogEntryDao;
-    this.userDao = userDao;
     this.modelService = modelService;
+    this.layerService = layerService;
     this.commentService = commentService;
     this.configurationService = configurationService;
     this.userService = userService;
-    this.chemicalService = chemicalService;
-    this.drugService = drugService;
-    this.modelAnnotator = modelAnnotator;
     this.dbUtils = dbUtils;
     this.meshParser = meshParser;
     this.taxonomyBackend = taxonomyBackend;
     this.projectBackgroundService = projectBackgroundService;
     this.minervaJobService = minervaJobService;
+    this.userAnnotationSchemaDao = userAnnotationSchemaDao;
   }
 
   @PostConstruct
@@ -278,7 +248,7 @@ public class ProjectService implements IProjectService {
 
   @Override
   public Project getProjectByProjectId(final String projectId, final boolean initializeLazy) {
-    final Project result = getProjectByProjectId(projectId);
+    final Project result = self.getProjectByProjectId(projectId);
     if (result != null && initializeLazy) {
       initLazy(result);
     }
@@ -287,7 +257,7 @@ public class ProjectService implements IProjectService {
 
   @Override
   public boolean projectExists(final String projectName) {
-    if (projectName == null || projectName.equals("")) {
+    if (projectName == null || projectName.isEmpty()) {
       return false;
     }
     return projectDao.isProjectExistsByName(projectName);
@@ -297,14 +267,14 @@ public class ProjectService implements IProjectService {
   public List<Project> getAllProjects() {
     final List<Project> projects = projectDao.getAll();
     for (final Project project : projects) {
-      project.getLogEntries().size();
+      Hibernate.initialize(project.getLogEntries());
     }
     return projects;
   }
 
   @Override
   public List<Project> getAllProjects(final boolean initializeLazy) {
-    final List<Project> result = getAllProjects();
+    final List<Project> result = self.getAllProjects();
     if (initializeLazy) {
       for (final Project project : result) {
         initLazy(project);
@@ -319,6 +289,7 @@ public class ProjectService implements IProjectService {
 
     final DeleteProjectMinervaJob jobParameters = new DeleteProjectMinervaJob(project.getProjectId(), homeDir);
     final MinervaJob job = new MinervaJob(MinervaJobType.DELETE_PROJECT, MinervaJobPriority.MEDIUM, jobParameters);
+    job.setExternalObject(project);
     minervaJobService.addJob(job);
     return job;
   }
@@ -378,7 +349,7 @@ public class ProjectService implements IProjectService {
       }
 
     } catch (final Exception e) {
-      self.projectFailure(project.getProjectId(), ProjectLogEntryType.OTHER,
+      self.projectFailure(projectId, ProjectLogEntryType.OTHER,
           "Severe problem with removing object. Underlying error:\n" + e.getMessage()
               + "\nMore information can be found in log file.",
           e);
@@ -414,7 +385,7 @@ public class ProjectService implements IProjectService {
       self.addLogEntries(params.getProjectId(), createLogEntries(appender));
 
       createRefreshDrugInfoJobs(params.getProjectId());
-      createRefreshChemicalInfoJobs(params.getProjectId());
+      submitRefreshChemicalInfoJobs(params.getProjectId());
       createRefreshMiriamInfoJobs(params.getProjectId());
 
       self.updateProjectStatus(params.getProjectId(), ProjectStatus.DONE, IProgressUpdater.MAX_PROGRESS);
@@ -435,10 +406,11 @@ public class ProjectService implements IProjectService {
               + "You might violated some unhandled constraints or you run out of memory. Underlying error:\n"
               + e.getMessage() + "\nMore information can be found in log file.",
           e);
-    } catch (final OutOfMemoryError oome) {
+    } catch (final OutOfMemoryError outOfMemoryError) {
       // release some memory
       outOfMemoryBuffer = null;
-      self.projectFailure(params.getProjectId(), ProjectLogEntryType.OUT_OF_MEMORY, "Out of memory: " + oome.getMessage(), oome);
+      self.projectFailure(params.getProjectId(), ProjectLogEntryType.OUT_OF_MEMORY,
+          "Out of memory: " + outOfMemoryError.getMessage(), outOfMemoryError);
     } catch (final Throwable e) {
       outOfMemoryBuffer = null;
       self.projectFailure(params.getProjectId(), ProjectLogEntryType.OTHER,
@@ -465,6 +437,7 @@ public class ProjectService implements IProjectService {
     final CreateProjectMinervaJob jobParameters = new CreateProjectMinervaJob(params);
 
     final MinervaJob job = new MinervaJob(MinervaJobType.CREATE_PROJECT, MinervaJobPriority.HIGH, jobParameters);
+    job.setExternalObject(project);
     minervaJobService.addJob(job);
 
     return job;
@@ -483,7 +456,7 @@ public class ProjectService implements IProjectService {
     }
   }
 
-  public void createRefreshChemicalInfoJobs(final String projectId) {
+  public void submitRefreshChemicalInfoJobs(final String projectId) {
     Project project = self.getProjectByProjectId(projectId, true);
     MiriamData disease = project.getDisease();
     if (disease != null) {
@@ -491,7 +464,9 @@ public class ProjectService implements IProjectService {
         minervaJobService.addJob(createRefreshChemicalInfoJob(miriamData, disease));
       }
       RefreshChemicalSuggestedQueryListMinervaJob parameters = new RefreshChemicalSuggestedQueryListMinervaJob(disease, project.getId());
-      minervaJobService.addJob(new MinervaJob(MinervaJobType.REFRESH_CHEMICAL_SUGGESTED_QUERY_LIST, MinervaJobPriority.LOWEST, parameters));
+      MinervaJob job = new MinervaJob(MinervaJobType.REFRESH_CHEMICAL_SUGGESTED_QUERY_LIST, MinervaJobPriority.LOWEST, parameters);
+      job.setExternalObject(project);
+      minervaJobService.addJob(job);
     }
   }
 
@@ -536,9 +511,9 @@ public class ProjectService implements IProjectService {
   @Override
   public void update(final Project project) {
     projectDao.update(project);
-    for (final ProjectBackground background : getBackgrounds(project.getProjectId(), false)) {
+    for (final ProjectBackground background : self.getBackgrounds(project.getProjectId(), false)) {
       background.setCreator(project.getOwner());
-      updateBackground(background);
+      self.updateBackground(background);
     }
     if (project.getOrganism() != null) {
       minervaJobService.addJob(createRefreshMiriamInfoJob(project.getOrganism()));
@@ -548,53 +523,6 @@ public class ProjectService implements IProjectService {
     }
   }
 
-  /**
-   * Retrieves (or creates) annotation schema for a given user.
-   *
-   * @return {@link UserAnnotationSchema} for {@link User}
-   */
-  @Override
-  public UserAnnotationSchema prepareUserAnnotationSchema(final User user, final boolean initializeLazy) {
-    UserAnnotationSchema annotationSchema = null;
-    if (user != null) {
-      annotationSchema = userDao.getById(user.getId()).getAnnotationSchema();
-      if (annotationSchema != null && annotationSchema.getClassAnnotators().size() == 0) {
-        for (final UserClassAnnotators uca : modelAnnotator.createDefaultAnnotatorSchema().getClassAnnotators()) {
-          annotationSchema.addClassAnnotator(uca);
-        }
-      }
-    }
-    if (annotationSchema == null) {
-      annotationSchema = modelAnnotator.createDefaultAnnotatorSchema();
-
-      final ElementUtils elementUtils = new ElementUtils();
-
-      final ClassTreeNode top = elementUtils.getAnnotatedElementClassTree();
-
-      final Map<Class<? extends BioEntity>, Set<MiriamType>> validMiriam = modelAnnotator.getDefaultValidClasses();
-      final Map<Class<? extends BioEntity>, Set<MiriamType>> requiredMiriam = modelAnnotator.getDefaultRequiredClasses();
-
-      final Queue<ClassTreeNode> nodes = new LinkedList<ClassTreeNode>();
-      nodes.add(top);
-
-      while (!nodes.isEmpty()) {
-        final ClassTreeNode element = nodes.poll();
-        annotationSchema.addClassAnnotator(new UserClassAnnotators(element.getClazz(),
-            modelAnnotator.getDefaultAnnotators(element.getClazz())));
-        nodes.addAll(element.getChildren());
-      }
-      if (user != null) {
-        final User dbUser = userDao.getById(user.getId());
-        dbUser.setAnnotationSchema(annotationSchema);
-        userDao.update(dbUser);
-      }
-    }
-    if (user != null) {
-      return userService.getUserByLogin(user.getLogin(), initializeLazy).getAnnotationSchema();
-    }
-    return annotationSchema;
-  }
-
   /**
    * This method creates set of images for the model backgrounds.
    *
@@ -609,9 +537,9 @@ public class ProjectService implements IProjectService {
     if (!params.isImages()) {
       return;
     }
-    final Project project = getProjectByProjectId(params.getProjectId());
+    final Project project = self.getProjectByProjectId(params.getProjectId());
     project.getTopModel();
-    updateProjectStatus(params.getProjectId(), ProjectStatus.GENERATING_IMAGES, 0);
+    self.updateProjectStatus(params.getProjectId(), ProjectStatus.GENERATING_IMAGES, 0);
 
     final int projectSize = project.getProjectBackgrounds().size();
     int counter = 0;
@@ -619,13 +547,11 @@ public class ProjectService implements IProjectService {
       final ProjectBackground background = project.getProjectBackgrounds().get(i);
       final double imgCounter = counter;
       final double finalSize = projectSize;
-      final IProgressUpdater updater = new IProgressUpdater() {
-        @Override
-        public void setProgress(final double progress) {
-          updateProjectStatus(params.getProjectId(), ProjectStatus.GENERATING_IMAGES,
-              IProgressUpdater.MAX_PROGRESS * imgCounter / finalSize + progress / finalSize);
-        }
-      };
+      final IProgressUpdater updater = progress -> self.updateProjectStatus(
+          params.getProjectId(),
+          ProjectStatus.GENERATING_IMAGES,
+          IProgressUpdater.MAX_PROGRESS * imgCounter / finalSize + progress / finalSize
+      );
       generateImagesForBuiltInBackground(background, updater);
       counter++;
     }
@@ -663,97 +589,54 @@ public class ProjectService implements IProjectService {
 
   @Override
   public Project createModel(final CreateProjectParams params)
-      throws InvalidInputDataExecption, ConverterException, IOException, InvalidDataOverlayException, InstantiationException, IllegalAccessException,
-      CommandExecutionException {
-    final Project project = getProjectByProjectId(params.getProjectId());
+      throws InvalidInputDataExecption, ConverterException, IOException, InvalidDataOverlayException, CommandExecutionException {
     final User dbUser = userService.getUserByLogin(params.getUserLogin());
     UserAnnotationSchema userAnnotationSchema = dbUser.getAnnotationSchema();
     if (userAnnotationSchema == null) {
       userAnnotationSchema = new UserAnnotationSchema();
     }
-    final UploadedFileEntry file = fileService.getById(params.getProjectFileId());
 
     if (params.isComplex()) {
-      try {
-        Class<? extends Converter> clazz = CellDesignerXmlParser.class;
-        if (params.getParser() != null) {
-          clazz = params.getParser();
-        }
-        final ComplexZipConverter parser = new ComplexZipConverter(clazz);
-        final ComplexZipConverterParams complexParams;
-        complexParams = new ComplexZipConverterParams().zipFile(file);
-        complexParams.visualizationDir(params.getProjectDir());
-        for (final ZipEntryFile entry : params.getZipEntries()) {
-          complexParams.entry(entry);
-        }
-        final ProjectFactory projectFactory = new ProjectFactory(parser);
-        projectFactory.create(complexParams, project);
-        for (final DataOverlay overlay : project.getDataOverlays()) {
-          overlay.setCreator(dbUser);
-        }
-      } catch (final IOException e) {
-        throw new InvalidInputDataExecption("Problem with processing input file", e);
-      }
+      self.createComplexProject(params);
     } else {
-      final Converter parser = params.getParser().newInstance();
-      if (parser == null) {
-        throw new InvalidArgumentException("Parser is undefined");
-      }
-      final InputStream input = new ByteArrayInputStream(file.getFileContent());
-      final Model model = parser
-          .createModel(new ConverterParams().inputStream(input, file.getOriginalFileName())
-              .sizeAutoAdjust(params.isAutoResize()).sbgnFormat(project.isSbgnFormat()));
-      project.addModel(model);
+      self.createSimpleProject(params);
     }
-    final Model topModel = project.getTopModel();
 
-    final Set<Model> models = new HashSet<>();
-    models.add(topModel);
-    for (final ModelSubmodelConnection connection : topModel.getSubmodelConnections()) {
-      models.add(connection.getSubmodel().getModel());
-    }
-    for (final Model m : models) {
-      for (final Layer l : m.getLayers()) {
-        if (!l.getName().equals(CreateHierarchyCommand.TEXT_LAYER_NAME)) {
-          l.setVisible(false);
-        }
+    Map<LayerProperty, Object> layerFilter = new HashMap<>();
+    layerFilter.put(LayerProperty.PROJECT_ID, params.getProjectId());
+    List<Layer> layers = layerService.getAll(layerFilter, Pageable.unpaged()).getContent();
+    for (final Layer layer : layers) {
+      if (!layer.getName().equals(CreateHierarchyCommand.TEXT_LAYER_NAME)) {
+        layer.setVisible(false);
+        layerService.update(layer);
       }
     }
-    fixProjectIssues(project);
 
-    assignZoomLevelDataToModel(topModel);
+    self.updateProjectStatus(params.getProjectId(), ProjectStatus.UPLOADING_TO_DB, 0.0);
 
-    updateProjectStatus(params.getProjectId(), ProjectStatus.UPLOADING_TO_DB, 0.0);
+    self.addBuiltInBackgrounds(params);
 
-    addBuiltInBackgrounds(params, project, dbUser);
-    self.update(project);
-    validateDataOverlays(project);
-    if (params.isUpdateAnnotations()) {
-      logger.debug("Updating annotations");
-      modelAnnotator.performAnnotations(topModel, new IProgressUpdater() {
+    self.validateDataOverlays(params.getProjectId());
 
-        @Override
-        public void setProgress(final double progress) {
-          updateProjectStatus(params.getProjectId(), ProjectStatus.ANNOTATING, progress);
-        }
-      }, userAnnotationSchema);
-      logger.debug("Annotations updated");
-    }
-    logger.debug("Model created");
 
-    final Model originalModel = project.getTopModel();
-    new CreateHierarchyCommand(originalModel, generator.computeZoomLevels(originalModel),
-        generator.computeZoomFactor(originalModel)).execute();
-    for (final Model model : originalModel.getSubmodels()) {
-      new CreateHierarchyCommand(model, generator.computeZoomLevels(model), generator.computeZoomFactor(model))
-          .execute();
+    self.createHierarchicalView(params.getProjectId());
+
+    self.updateProjectStatus(params.getProjectId(), ProjectStatus.UPLOADING_TO_DB, 100.0);
+
+    logger.trace("Model {} created", params.getProjectId());
+
+    if (params.isUpdateAnnotations()) {
+      self.submitAnnotateProjectJob(params.getProjectId(), userAnnotationSchema);
     }
-    logger.debug("Hierarchy view added");
-    return project;
+
+    return self.getProjectByProjectId(params.getProjectId());
   }
 
-  private void addBuiltInBackgrounds(final CreateProjectParams params, final Project project, final User dbUser)
+  @Override
+  public void addBuiltInBackgrounds(final CreateProjectParams params)
       throws InvalidInputDataExecption {
+    User dbUser = userService.getUserByLogin(params.getUserLogin());
+    Project project = self.getProjectByProjectId(params.getProjectId());
     final List<BuildInBackgrounds> buildInBackgrounds = new ArrayList<>();
     if (params.isNetworkBackgroundAsDefault()) {
       buildInBackgrounds.add(BuildInBackgrounds.NORMAL);
@@ -786,7 +669,7 @@ public class ProjectService implements IProjectService {
         final String directory = params.getProjectDir() + "/" + buildInBackground.getDirectorySuffix() + submodelId + "/";
         background.addProjectBackgroundImageLayer(new ProjectBackgroundImageLayer(connection.getSubmodel(), directory));
 
-        assignZoomLevelDataToModel(connection.getSubmodel().getModel());
+        assignZoomLevelDataToModel(connection.getSubmodel().getModel().getModelData());
         submodelId++;
       }
     }
@@ -796,7 +679,9 @@ public class ProjectService implements IProjectService {
     }
   }
 
-  private void validateDataOverlays(final Project project) throws IOException, InvalidDataOverlayException {
+  @Override
+  public void validateDataOverlays(final String projectId) throws IOException, InvalidDataOverlayException {
+    Project project = self.getProjectByProjectId(projectId);
     for (final DataOverlay l : project.getDataOverlays()) {
       if (l.getInputData() != null) {
         final ColorSchemaReader reader = new ColorSchemaReader();
@@ -811,11 +696,79 @@ public class ProjectService implements IProjectService {
     }
   }
 
-  private void assignZoomLevelDataToModel(final Model topModel) throws InvalidInputDataExecption {
-    final Integer maxZoomLevels = Integer
+  @Override
+  public void createHierarchicalView(final String projectId) throws CommandExecutionException {
+    Project project = self.getProjectByProjectId(projectId);
+    final Model originalModel = project.getTopModel();
+    new CreateHierarchyCommand(originalModel, generator.computeZoomLevels(originalModel.getModelData()),
+        generator.computeZoomFactor(originalModel.getModelData())).execute();
+    for (final Model model : originalModel.getSubmodels()) {
+      new CreateHierarchyCommand(model, generator.computeZoomLevels(model.getModelData()), generator.computeZoomFactor(model.getModelData()))
+          .execute();
+    }
+    self.update(project);
+    logger.debug("Hierarchy view added");
+  }
+
+  @Override
+  public void createComplexProject(final CreateProjectParams params) throws InvalidInputDataExecption {
+    try {
+      final UploadedFileEntry file = fileService.getById(params.getProjectFileId());
+      Project project = self.getProjectByProjectId(params.getProjectId());
+      User dbUser = userService.getUserByLogin(params.getUserLogin());
+
+      Class<? extends Converter> clazz = CellDesignerXmlParser.class;
+      if (params.getParser() != null) {
+        clazz = params.getParser();
+      }
+      final ComplexZipConverter parser = new ComplexZipConverter(clazz);
+      final ComplexZipConverterParams complexParams;
+      complexParams = new ComplexZipConverterParams().zipFile(file);
+      complexParams.visualizationDir(params.getProjectDir());
+      for (final ZipEntryFile entry : params.getZipEntries()) {
+        complexParams.entry(entry);
+      }
+      final ProjectFactory projectFactory = new ProjectFactory(parser);
+      projectFactory.create(complexParams, project);
+      for (final DataOverlay overlay : project.getDataOverlays()) {
+        overlay.setCreator(dbUser);
+      }
+      for (ModelData modelData : project.getModels()) {
+        project.addLogEntries(fixModelIssues(modelData));
+      }
+      self.update(project);
+    } catch (final IOException | ConverterException e) {
+      throw new InvalidInputDataExecption("Problem with processing input file", e);
+    }
+  }
+
+  @Override
+  public void createSimpleProject(final CreateProjectParams params) throws ConverterException {
+    try {
+      final UploadedFileEntry file = fileService.getById(params.getProjectFileId());
+      Project project = self.getProjectByProjectId(params.getProjectId());
+
+      final Converter parser = params.getParser().newInstance();
+      final InputStream input = new ByteArrayInputStream(file.getFileContent());
+      final Model model = parser.createModel(new ConverterParams()
+          .inputStream(input, file.getOriginalFileName())
+          .sizeAutoAdjust(params.isAutoResize())
+          .sbgnFormat(project.isSbgnFormat()));
+      project.addLogEntries(fixModelIssues(model.getModelData()));
+      assignZoomLevelDataToModel(model.getModelData());
+      modelService.add(model.getModelData());
+      project.setTopModel(model);
+      self.update(project);
+    } catch (InvalidInputDataExecption | InstantiationException | IllegalAccessException e) {
+      throw new ConverterException(e);
+    }
+  }
+
+  private void assignZoomLevelDataToModel(final ModelData topModel) throws InvalidInputDataExecption {
+    final int maxZoomLevels = Integer
         .parseInt(configurationService.getValue(ConfigurationElementType.MAX_NUMBER_OF_MAP_LEVELS).getValue());
 
-    final Integer zoomLevels = generator.computeZoomLevels(topModel);
+    final int zoomLevels = generator.computeZoomLevels(topModel);
     if (zoomLevels > maxZoomLevels) {
       throw new InvalidInputDataExecption(
           "Map " + topModel.getName() + " too big. You can change the max number of map zoom levels in configuration.");
@@ -834,42 +787,25 @@ public class ProjectService implements IProjectService {
    */
   @Override
   public void updateProjectStatus(final String projectId, final ProjectStatus status, final double progress) {
-    final Project project = getProjectByProjectId(projectId);
+    final Project project = self.getProjectByProjectId(projectId);
     if (project != null) {
       if (!status.equals(project.getStatus())
           || (Math.abs(progress - project.getProgress()) > IProgressUpdater.PROGRESS_BAR_UPDATE_RESOLUTION)) {
         project.setStatus(status);
         project.setProgress(progress);
-        fixProjectIssues(project);
         self.update(project);
       }
     } else {
-      logger.debug("status: " + status + ", " + progress);
+      logger.trace("Update project status: {}, {}", status, progress);
     }
   }
 
-  /**
-   * @return the projectDao
-   * @see #projectDao
-   */
-  public ProjectDao getProjectDao() {
-    return projectDao;
-  }
-
-  /**
-   * @param projectDao the projectDao to set
-   * @see #projectDao
-   */
-  public void setProjectDao(final ProjectDao projectDao) {
-    this.projectDao = projectDao;
-  }
-
   /**
    * Sends notification email that map was removed.
    *
    * @param projectId identifier of the project
    * @param email     notification email
-   * @throws MessagingException thrown when there is a problem with sending email
+   * @throws EmailException thrown when there is a problem with sending email
    */
   protected void sendSuccesfullRemoveEmail(final String projectId, final String email) throws EmailException {
     final EmailSender emailSender = new EmailSender(configurationService);
@@ -964,6 +900,23 @@ public class ProjectService implements IProjectService {
     return result;
   }
 
+  private ProjectLogEntry createLogEntry(final Pair<Object, String> pair) {
+    final ProjectLogEntry entry = new ProjectLogEntry();
+    entry.setContent(pair.getRight());
+    final Object left = pair.getLeft();
+    if (left instanceof BioEntity) {
+      final BioEntity bioEntity = (BioEntity) left;
+      entry.setMapName(bioEntity.getModelData().getName());
+      entry.setObjectClass(bioEntity.getClass().getSimpleName());
+      entry.setObjectIdentifier(bioEntity.getElementId());
+    } else if (left instanceof ModelData) {
+      entry.setMapName(((ModelData) left).getName());
+    }
+    entry.setSeverity("WARNING");
+    entry.setType(ProjectLogEntryType.DATABASE_PROBLEM);
+    return entry;
+  }
+
   private ProjectLogEntry createLogEntry(final String severity, final LogEvent event) {
     if (event.getMarker() instanceof IgnoredLogMarker) {
       return null;
@@ -983,30 +936,31 @@ public class ProjectService implements IProjectService {
     return entry;
   }
 
+  List<ProjectLogEntry> fixModelIssues(final ModelData model) {
+    List<ProjectLogEntry> result = new ArrayList<>();
+    final ObjectValidator validator = new ObjectValidator();
+    final List<Pair<Object, String>> issues = validator.getValidationIssues(model);
+    if (!issues.isEmpty()) {
+      for (final Pair<Object, String> pair : issues) {
+        result.add(createLogEntry(pair));
+      }
+    }
+    validator.fixValidationIssues(model);
+    return result;
+  }
+
   void fixProjectIssues(final Project project) {
     final ObjectValidator validator = new ObjectValidator();
     final List<Pair<Object, String>> issues = validator.getValidationIssues(project);
-    if (issues.size() > 0) {
+    if (!issues.isEmpty()) {
       for (final Pair<Object, String> pair : issues) {
-        final ProjectLogEntry entry = new ProjectLogEntry();
-        entry.setContent(pair.getRight());
-        final Object left = pair.getLeft();
-        if (left instanceof BioEntity) {
-          final BioEntity bioEntity = (BioEntity) left;
-          entry.setMapName(bioEntity.getModelData().getName());
-          entry.setObjectClass(bioEntity.getClass().getSimpleName());
-          entry.setObjectIdentifier(bioEntity.getElementId());
-        } else if (left instanceof ModelData) {
-          entry.setMapName(((ModelData) left).getName());
-        }
-        entry.setSeverity("WARNING");
-        entry.setType(ProjectLogEntryType.DATABASE_PROBLEM);
-        project.addLogEntry(entry);
+        project.addLogEntry(createLogEntry(pair));
       }
     }
     validator.fixValidationIssues(project);
   }
 
+
   @Override
   public void removeBackground(final ProjectBackground projectBackground) {
     projectBackgroundService.delete(projectBackground);
@@ -1053,7 +1007,7 @@ public class ProjectService implements IProjectService {
 
   @Override
   public UploadedFileEntry getFileByProjectId(final String projectId) {
-    final Project project = getProjectByProjectId(projectId);
+    final Project project = self.getProjectByProjectId(projectId);
     if (project == null) {
       return null;
     }
@@ -1063,7 +1017,7 @@ public class ProjectService implements IProjectService {
 
   @Override
   public List<ProjectBackground> getBackgrounds(final String projectId, final boolean initializeLazy) {
-    final Project project = getProjectByProjectId(projectId);
+    final Project project = self.getProjectByProjectId(projectId);
     if (project != null) {
       for (final ProjectBackground projectBackground : project.getProjectBackgrounds()) {
         if (initializeLazy) {
@@ -1109,7 +1063,7 @@ public class ProjectService implements IProjectService {
   public void execute(final MinervaJob job) throws Exception {
     if (job.getJobType() == MinervaJobType.DELETE_PROJECT) {
       final DeleteProjectMinervaJob parameters = new DeleteProjectMinervaJob(job.getJobParameters());
-      removeProject(parameters.getProjectId(), parameters.getDirectory());
+      self.removeProject(parameters.getProjectId(), parameters.getDirectory());
     } else if (job.getJobType() == MinervaJobType.CREATE_PROJECT) {
       final CreateProjectMinervaJob parameters = new CreateProjectMinervaJob(job.getJobParameters());
       createProject(parameters.getParameters());
@@ -1146,12 +1100,12 @@ public class ProjectService implements IProjectService {
   public void addLogEntries(final String projectId, final Set<ProjectLogEntry> createLogEntries) {
     final Project project = self.getProjectByProjectId(projectId);
     project.addLogEntries(createLogEntries);
-    update(project);
+    self.update(project);
   }
 
   @Override
   public void submitArchiveProjectJob(final String projectId) {
-    final Project project = getProjectByProjectId(projectId);
+    final Project project = self.getProjectByProjectId(projectId);
     final String homeDir = getProjectHomeDir(project);
 
     project.setStatus(ProjectStatus.ARCHIVED);
@@ -1159,10 +1113,24 @@ public class ProjectService implements IProjectService {
     for (final ProjectBackground background : project.getProjectBackgrounds()) {
       final DeleteBackgroundMinervaJob jobParameters = new DeleteBackgroundMinervaJob(background.getId(), homeDir);
       final MinervaJob job = new MinervaJob(MinervaJobType.DELETE_BACKGROUND, MinervaJobPriority.LOW, jobParameters);
+      job.setExternalObject(project);
       minervaJobService.addJob(job);
     }
   }
 
+  @Override
+  public void submitAnnotateProjectJob(final String projectId, final UserAnnotationSchema schema) {
+    UserAnnotationSchema userAnnotationSchema = schema;
+    if (schema != null && schema.getId() != 0) {
+      userAnnotationSchema = userAnnotationSchemaDao.getById(schema.getId());
+    }
+    Project project = self.getProjectByProjectId(projectId);
+    final AnnotateProjectMinervaJob jobParameters = new AnnotateProjectMinervaJob(projectId, userAnnotationSchema);
+    final MinervaJob job = new MinervaJob(MinervaJobType.ANNOTATE_PROJECT, MinervaJobPriority.MEDIUM, jobParameters);
+    job.setExternalObject(project);
+    minervaJobService.addJob(job);
+  }
+
   @Override
   public String getProjectHomeDir(final Project project) {
     final String homeDir;
@@ -1177,26 +1145,28 @@ public class ProjectService implements IProjectService {
 
   @Override
   public void submitReviveProjectJob(final String projectId) {
-    final Project project = getProjectByProjectId(projectId);
+    final Project project = self.getProjectByProjectId(projectId);
 
     project.setStatus(ProjectStatus.GENERATING_IMAGES);
 
     final ReviveBackgroundsMinervaJob jobParameters = new ReviveBackgroundsMinervaJob(projectId);
     final MinervaJob job = new MinervaJob(MinervaJobType.REVIVE_BACKGROUNDS, MinervaJobPriority.MEDIUM, jobParameters);
+    job.setExternalObject(project);
     minervaJobService.addJob(job);
   }
 
   @Override
   public void reviveBackgrounds(final String projectId) {
     try {
-      final Project project = getProjectByProjectId(projectId);
+      final Project project = self.getProjectByProjectId(projectId);
       final CreateProjectParams params = new CreateProjectParams()
           .projectId(projectId)
+          .setUser(project.getOwner())
           .projectDir(getProjectHomeDir(project))
           .images(true);
 
-      addBuiltInBackgrounds(params, project, project.getOwner());
-      createImages(params);
+      self.addBuiltInBackgrounds(params);
+      self.createImages(params);
 
       project.setStatus(ProjectStatus.DONE);
     } catch (final Exception e) {
diff --git a/service/src/main/java/lcsb/mapviewer/services/interfaces/IAnnotationService.java b/service/src/main/java/lcsb/mapviewer/services/interfaces/IAnnotationService.java
new file mode 100644
index 0000000000000000000000000000000000000000..467bb0f88e79b7e7ecdd76021618e2d55932486c
--- /dev/null
+++ b/service/src/main/java/lcsb/mapviewer/services/interfaces/IAnnotationService.java
@@ -0,0 +1,18 @@
+package lcsb.mapviewer.services.interfaces;
+
+import lcsb.mapviewer.model.job.MinervaJobExecutor;
+import lcsb.mapviewer.model.user.User;
+import lcsb.mapviewer.model.user.UserAnnotationSchema;
+import lcsb.mapviewer.services.jobs.AnnotateProjectMinervaJob;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.function.Consumer;
+
+public interface IAnnotationService extends MinervaJobExecutor {
+
+  @Transactional
+  UserAnnotationSchema prepareUserAnnotationSchema(final User user, final boolean initializeLazy);
+
+  void handleAnnotateProjectJob(final AnnotateProjectMinervaJob parameters, Consumer<Double> updateProgress);
+
+}
diff --git a/service/src/main/java/lcsb/mapviewer/services/interfaces/IMinervaJobService.java b/service/src/main/java/lcsb/mapviewer/services/interfaces/IMinervaJobService.java
index a6da66f3c85933d1f9763ffd165333a8d09a15d8..cf81e304f49afd17b7ed3203020cc430cdb8bd8a 100644
--- a/service/src/main/java/lcsb/mapviewer/services/interfaces/IMinervaJobService.java
+++ b/service/src/main/java/lcsb/mapviewer/services/interfaces/IMinervaJobService.java
@@ -5,12 +5,13 @@ import lcsb.mapviewer.model.job.MinervaJob;
 import lcsb.mapviewer.model.job.MinervaJobExecutor;
 import lcsb.mapviewer.model.job.MinervaJobStatus;
 import lcsb.mapviewer.model.job.MinervaJobType;
+import lcsb.mapviewer.persist.dao.MinervaJobProperty;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.util.Calendar;
 import java.util.List;
 
-public interface IMinervaJobService {
+public interface IMinervaJobService extends CrudService<MinervaJob, MinervaJobProperty> {
 
   boolean addJob(MinervaJob job);
 
@@ -51,4 +52,7 @@ public interface IMinervaJobService {
   void enableQueue(boolean shutdownOnFailure);
 
   void disableQueue();
+
+  @Transactional
+  void updateJobProgress(MinervaJob job, Double progress);
 }
diff --git a/service/src/main/java/lcsb/mapviewer/services/interfaces/IProjectService.java b/service/src/main/java/lcsb/mapviewer/services/interfaces/IProjectService.java
index fc153a2c3724f8c0b80af947b7c0509d53537673..0f73a7bcca41dc17fcdac39724d66f7593159904 100644
--- a/service/src/main/java/lcsb/mapviewer/services/interfaces/IProjectService.java
+++ b/service/src/main/java/lcsb/mapviewer/services/interfaces/IProjectService.java
@@ -14,7 +14,6 @@ import lcsb.mapviewer.model.job.MinervaJobExecutor;
 import lcsb.mapviewer.model.map.MiriamData;
 import lcsb.mapviewer.model.map.layout.ProjectBackground;
 import lcsb.mapviewer.model.overlay.InvalidDataOverlayException;
-import lcsb.mapviewer.model.user.User;
 import lcsb.mapviewer.model.user.UserAnnotationSchema;
 import lcsb.mapviewer.persist.dao.ProjectLogEntryProperty;
 import lcsb.mapviewer.persist.dao.ProjectProperty;
@@ -74,12 +73,8 @@ public interface IProjectService extends MinervaJobExecutor, CrudService<Project
   @Transactional
   MinervaJob submitCreateProjectJob(final CreateProjectParams params) throws SecurityException;
 
-  @Transactional
   Project createProject(final CreateProjectParams params) throws SecurityException;
 
-  @Transactional
-  UserAnnotationSchema prepareUserAnnotationSchema(final User user, final boolean initializeLazy);
-
   @Transactional
   void removeBackground(final ProjectBackground projectBackground);
 
@@ -110,7 +105,6 @@ public interface IProjectService extends MinervaJobExecutor, CrudService<Project
   @Transactional
   void removeProject(String projectId, String directory);
 
-  @Transactional
   Project createModel(CreateProjectParams params)
       throws InvalidInputDataExecption, ConverterException, IOException, InvalidDataOverlayException, InstantiationException, IllegalAccessException,
       CommandExecutionException;
@@ -168,4 +162,24 @@ public interface IProjectService extends MinervaJobExecutor, CrudService<Project
 
   @Transactional
   Page<ProjectLogEntry> getLogEntries(Map<ProjectLogEntryProperty, Object> searchFilter, Pageable pageable);
+
+  @Transactional
+  void addBuiltInBackgrounds(final CreateProjectParams params) throws InvalidInputDataExecption;
+
+  @Transactional
+  void validateDataOverlays(final String projectId) throws IOException, InvalidDataOverlayException;
+
+  @Transactional
+  void createHierarchicalView(final String projectId) throws CommandExecutionException;
+
+  @Transactional
+  void createComplexProject(CreateProjectParams params) throws InvalidInputDataExecption;
+
+  @Transactional
+  void createSimpleProject(CreateProjectParams params) throws ConverterException;
+
+  @Transactional
+  void submitAnnotateProjectJob(final String projectId, final UserAnnotationSchema schema);
+
+  void submitRefreshChemicalInfoJobs(final String projectId);
 }
diff --git a/service/src/main/java/lcsb/mapviewer/services/jobs/AnnotateProjectMinervaJob.java b/service/src/main/java/lcsb/mapviewer/services/jobs/AnnotateProjectMinervaJob.java
new file mode 100644
index 0000000000000000000000000000000000000000..46dcc914bee8426680d810112a6f22217c794931
--- /dev/null
+++ b/service/src/main/java/lcsb/mapviewer/services/jobs/AnnotateProjectMinervaJob.java
@@ -0,0 +1,73 @@
+package lcsb.mapviewer.services.jobs;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lcsb.mapviewer.model.job.InvalidMinervaJobParameters;
+import lcsb.mapviewer.model.job.MinervaJobParameters;
+import lcsb.mapviewer.model.user.UserAnnotationSchema;
+import org.apache.logging.log4j.LogManager;
+import org.apache.logging.log4j.Logger;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class AnnotateProjectMinervaJob extends MinervaJobParameters {
+
+  private static final Logger logger = LogManager.getLogger();
+
+  private String projectId;
+
+  private UserAnnotationSchema schema;
+
+  public AnnotateProjectMinervaJob(final String projectId, final UserAnnotationSchema schema) {
+    this.projectId = projectId;
+    this.schema = schema;
+  }
+
+  public AnnotateProjectMinervaJob(final Map<String, Object> jobParameters) throws InvalidMinervaJobParameters {
+    super(jobParameters);
+  }
+
+  @Override
+  protected void assignData(final Map<String, Object> jobParameters) {
+    ObjectMapper mapper = new ObjectMapper();
+
+    this.projectId = (String) jobParameters.get("projectId");
+    Object schemaData = jobParameters.get("schema");
+    if (schemaData != null) {
+      try {
+        String serializedSchema = mapper.writeValueAsString(schemaData);
+        this.schema = mapper.readValue(serializedSchema, new TypeReference<UserAnnotationSchema>() {
+        });
+      } catch (Exception e) {
+        logger.error("Problem with deserializing task", e);
+        this.schema = null;
+      }
+    }
+  }
+
+  @Override
+  protected Map<String, Object> getJobParameters() {
+    ObjectMapper mapper = new ObjectMapper();
+    Map<String, Object> jobParameters = new HashMap<>();
+    jobParameters.put("projectId", projectId);
+    try {
+      String serializedSchema = mapper.writeValueAsString(schema);
+      jobParameters.put("schema", mapper.readValue(serializedSchema, new TypeReference<Map<String, Object>>() {
+      }));
+    } catch (Exception e) {
+      logger.error("Problem with serializing task", e);
+      this.schema = null;
+    }
+
+    return jobParameters;
+  }
+
+  public String getProjectId() {
+    return projectId;
+  }
+
+  public UserAnnotationSchema getSchema() {
+    return schema;
+  }
+}
diff --git a/service/src/main/java/lcsb/mapviewer/services/search/chemical/ChemicalService.java b/service/src/main/java/lcsb/mapviewer/services/search/chemical/ChemicalService.java
index df9615befa8998026fcd0c95ace310c790ba966f..14983096fe746a592c12b8331fa0ea4e64b89198 100644
--- a/service/src/main/java/lcsb/mapviewer/services/search/chemical/ChemicalService.java
+++ b/service/src/main/java/lcsb/mapviewer/services/search/chemical/ChemicalService.java
@@ -171,7 +171,9 @@ public class ChemicalService extends DbSearchService implements IChemicalService
     if (result == null) {
       chemicalParser.setChemicalSuggestedQueryList(project.getId(), disease, Collections.singletonList(" "));
       RefreshChemicalSuggestedQueryListMinervaJob parameters = new RefreshChemicalSuggestedQueryListMinervaJob(disease, project.getId());
-      minervaJobService.addJob(new MinervaJob(MinervaJobType.REFRESH_CHEMICAL_SUGGESTED_QUERY_LIST, MinervaJobPriority.LOWEST, parameters));
+      MinervaJob job = new MinervaJob(MinervaJobType.REFRESH_CHEMICAL_SUGGESTED_QUERY_LIST, MinervaJobPriority.LOWEST, parameters);
+      job.setExternalObject(project);
+      minervaJobService.addJob(job);
       result = new ArrayList<>();
     }
     return result;
diff --git a/service/src/main/java/lcsb/mapviewer/services/search/drug/DrugService.java b/service/src/main/java/lcsb/mapviewer/services/search/drug/DrugService.java
index 4ca81e45d23318d13df8a401fd01b664de235e92..9be0ef00d02263a522649146a8bf9e090287f041 100644
--- a/service/src/main/java/lcsb/mapviewer/services/search/drug/DrugService.java
+++ b/service/src/main/java/lcsb/mapviewer/services/search/drug/DrugService.java
@@ -181,7 +181,7 @@ public class DrugService extends DbSearchService implements IDrugService {
 
   @Override
   public Drug getByName(final String name, final DbSearchCriteria searchCriteria) {
-    if (name.trim().equals("")) {
+    if (name.trim().isEmpty()) {
       return null;
     }
     DrugAnnotation secondParser = chEMBLParser;
@@ -253,7 +253,7 @@ public class DrugService extends DbSearchService implements IDrugService {
       addProjectTargets(searchCriteria.getProject(), drug);
     }
 
-    Collections.sort(drugList, new Drug.NameComparator());
+    drugList.sort(new Drug.NameComparator());
     return drugList;
   }
 
@@ -285,8 +285,7 @@ public class DrugService extends DbSearchService implements IDrugService {
     List<String> chemblList = chEMBLParser.getSuggestedQueryList(project, organism);
     if (drugBankList == null) {
       drugBankParser.setDrugSuggestedQueryList(project.getId(), organism, Collections.singletonList(" "));
-      RefreshDrugSuggestedQueryListMinervaJob parameters = new RefreshDrugSuggestedQueryListMinervaJob(organism, project.getId());
-      minervaJobService.addJob(new MinervaJob(MinervaJobType.REFRESH_DRUG_BANK_SUGGESTED_QUERY_LIST, MinervaJobPriority.LOWEST, parameters));
+      submitRefreshDrugBankSuggestedQueryList(organism, project);
     } else {
       for (final String string : drugBankList) {
         resultSet.add(string.toLowerCase());
@@ -295,8 +294,7 @@ public class DrugService extends DbSearchService implements IDrugService {
 
     if (chemblList == null) {
       chEMBLParser.setDrugSuggestedQueryList(project.getId(), organism, Collections.singletonList(" "));
-      RefreshDrugSuggestedQueryListMinervaJob parameters = new RefreshDrugSuggestedQueryListMinervaJob(organism, project.getId());
-      minervaJobService.addJob(new MinervaJob(MinervaJobType.REFRESH_CHEMBL_SUGGESTED_QUERY_LIST, MinervaJobPriority.LOWEST, parameters));
+      submitRefreshChemblSuggestedQueryList(organism, project);
     } else {
       for (final String string : chemblList) {
         resultSet.add(string.toLowerCase());
@@ -308,6 +306,22 @@ public class DrugService extends DbSearchService implements IDrugService {
     return result;
   }
 
+  @Override
+  public void submitRefreshChemblSuggestedQueryList(final MiriamData organism, final Project project) {
+    RefreshDrugSuggestedQueryListMinervaJob parameters = new RefreshDrugSuggestedQueryListMinervaJob(organism, project.getId());
+    MinervaJob job = new MinervaJob(MinervaJobType.REFRESH_CHEMBL_SUGGESTED_QUERY_LIST, MinervaJobPriority.LOWEST, parameters);
+    job.setExternalObject(project);
+    minervaJobService.addJob(job);
+  }
+
+  @Override
+  public void submitRefreshDrugBankSuggestedQueryList(final MiriamData organism, final Project project) {
+    RefreshDrugSuggestedQueryListMinervaJob parameters = new RefreshDrugSuggestedQueryListMinervaJob(organism, project.getId());
+    MinervaJob job = new MinervaJob(MinervaJobType.REFRESH_DRUG_BANK_SUGGESTED_QUERY_LIST, MinervaJobPriority.LOWEST, parameters);
+    job.setExternalObject(project);
+    minervaJobService.addJob(job);
+  }
+
   @Override
   public void execute(final MinervaJob job) throws Exception {
     if (job.getJobType() == MinervaJobType.REFRESH_DRUG_INFO) {
diff --git a/service/src/main/java/lcsb/mapviewer/services/search/drug/IDrugService.java b/service/src/main/java/lcsb/mapviewer/services/search/drug/IDrugService.java
index 99e3407ece37b96115dab339b6b36aea0ec439b0..6cfe842af73ace439828f5f344f1f7ed55fb5d35 100644
--- a/service/src/main/java/lcsb/mapviewer/services/search/drug/IDrugService.java
+++ b/service/src/main/java/lcsb/mapviewer/services/search/drug/IDrugService.java
@@ -1,7 +1,5 @@
 package lcsb.mapviewer.services.search.drug;
 
-import java.util.List;
-
 import lcsb.mapviewer.annotation.data.Drug;
 import lcsb.mapviewer.annotation.services.DrugSearchException;
 import lcsb.mapviewer.model.Project;
@@ -9,14 +7,18 @@ import lcsb.mapviewer.model.job.MinervaJobExecutor;
 import lcsb.mapviewer.model.map.MiriamData;
 import lcsb.mapviewer.services.search.IDbSearchService;
 
+import java.util.List;
+
 /**
  * Service for accessing information about drugs.
- * 
+ *
  * @author Piotr Gawron
- * 
  */
 public interface IDrugService extends IDbSearchService<Drug>, MinervaJobExecutor {
 
   List<String> getSuggestedQueryList(final Project project, final MiriamData disease) throws DrugSearchException;
 
+  void submitRefreshChemblSuggestedQueryList(final MiriamData organism, final Project project);
+
+  void submitRefreshDrugBankSuggestedQueryList(final MiriamData organism, final Project project);
 }
diff --git a/service/src/test/java/lcsb/mapviewer/services/impl/MinervaJobServiceTest.java b/service/src/test/java/lcsb/mapviewer/services/impl/MinervaJobServiceTest.java
index 88d63c13fb13efe4d3074307bb55bbdcec8b6441..cad28d28e52529d650a9ae280191d7e30393e04e 100644
--- a/service/src/test/java/lcsb/mapviewer/services/impl/MinervaJobServiceTest.java
+++ b/service/src/test/java/lcsb/mapviewer/services/impl/MinervaJobServiceTest.java
@@ -5,6 +5,7 @@ import lcsb.mapviewer.annotation.cache.GeneralCacheInterface;
 import lcsb.mapviewer.annotation.services.annotators.HgncAnnotator;
 import lcsb.mapviewer.common.tests.TestUtils;
 import lcsb.mapviewer.common.tests.UnitTestFailedWatcher;
+import lcsb.mapviewer.model.Project;
 import lcsb.mapviewer.model.cache.CacheQuery;
 import lcsb.mapviewer.model.cache.CacheType;
 import lcsb.mapviewer.model.job.MinervaJob;
@@ -14,6 +15,7 @@ import lcsb.mapviewer.model.job.MinervaJobStatus;
 import lcsb.mapviewer.model.job.MinervaJobType;
 import lcsb.mapviewer.model.map.MiriamData;
 import lcsb.mapviewer.model.map.MiriamType;
+import lcsb.mapviewer.model.user.User;
 import lcsb.mapviewer.services.SpringServiceTestConfig;
 import lcsb.mapviewer.services.interfaces.IMinervaJobService;
 import lcsb.mapviewer.services.interfaces.IMiriamService;
@@ -205,4 +207,67 @@ public class MinervaJobServiceTest extends TestUtils {
     }
   }
 
+  @Test
+  public void testUpdateProgress() {
+    minervaJobService.disableQueue();
+    try {
+      RefreshMiriamInfoMinervaJob params = new RefreshMiriamInfoMinervaJob(-1);
+      MinervaJob job = new MinervaJob(MinervaJobType.REFRESH_MIRIAM_INFO, MinervaJobPriority.MEDIUM, params);
+      minervaJobService.addJob(job);
+
+      minervaJobService.updateJobProgress(job, 50.0);
+
+      job = minervaJobService.getJobById(job.getId());
+      assertEquals(50.0, job.getProgress(), EPSILON);
+    } finally {
+      minervaJobService.enableQueue(false);
+    }
+  }
+
+  @Test
+  public void testUpdateProgressInvalidJob() {
+    minervaJobService.disableQueue();
+    try {
+      RefreshMiriamInfoMinervaJob params = new RefreshMiriamInfoMinervaJob(-1);
+      MinervaJob job = new MinervaJob(MinervaJobType.REFRESH_MIRIAM_INFO, MinervaJobPriority.MEDIUM, params);
+
+      minervaJobService.updateJobProgress(job, 50.0);
+
+      assertNull(minervaJobService.getJobById(job.getId()));
+    } finally {
+      minervaJobService.enableQueue(false);
+    }
+  }
+
+  @Test
+  public void testAddJobWithExternalObject() {
+    Project project = createProject();
+    minervaJobService.disableQueue();
+    try {
+      RefreshMiriamInfoMinervaJob params = new RefreshMiriamInfoMinervaJob(-1);
+      MinervaJob job = new MinervaJob(MinervaJobType.REFRESH_MIRIAM_INFO, MinervaJobPriority.MEDIUM, params);
+      job.setExternalObject(project);
+
+      minervaJobService.addJob(job);
+
+      job = minervaJobService.getJobById(job.getId());
+
+      assertNotNull(job.getExternalObjectClass());
+      assertNotNull(job.getExternalObjectId());
+    } finally {
+      minervaJobService.enableQueue(false);
+    }
+  }
+
+  public Project createProject() {
+    User admin = new User();
+    admin.setId(BUILT_IN_TEST_ADMIN_ID);
+
+    Project project = new Project();
+    project.setProjectId(faker.numerify("T######"));
+    project.setOwner(admin);
+
+    return project;
+  }
+
 }
diff --git a/service/src/test/java/lcsb/mapviewer/services/impl/ProjectServiceNoTransactionTest.java b/service/src/test/java/lcsb/mapviewer/services/impl/ProjectServiceNoTransactionTest.java
index 7a697ee84b6038ec66ea40af8758eb9e5acff678..000a7709de727c9ec0257db9e571669131b4dacb 100644
--- a/service/src/test/java/lcsb/mapviewer/services/impl/ProjectServiceNoTransactionTest.java
+++ b/service/src/test/java/lcsb/mapviewer/services/impl/ProjectServiceNoTransactionTest.java
@@ -538,7 +538,7 @@ public class ProjectServiceNoTransactionTest extends TestUtils {
 
       ProjectService projectService = new ProjectService(null, mockProjectDao,
           Mockito.mock(ProjectLogEntryDao.class),
-          null, null, null, null, null, null, null, null, null, null, null, Mockito.mock(ProjectBackgroundService.class), null);
+          null, null, null, null, null, null, null, null, Mockito.mock(ProjectBackgroundService.class), null, null);
       ReflectionTestUtils.setField(projectService, "self", projectService);
 
       projectService.createImages(params);
@@ -637,8 +637,8 @@ public class ProjectServiceNoTransactionTest extends TestUtils {
     GenericProtein protein = new GenericProtein("1");
     project.addModel(model);
     model.addElement(protein);
-    service.fixProjectIssues(project);
-    assertFalse(project.getLogEntries().isEmpty());
+    List<ProjectLogEntry> entries = service.fixModelIssues(model);
+    assertFalse(entries.isEmpty());
   }
 
 
diff --git a/service/src/test/java/lcsb/mapviewer/services/jobs/AnnotateProjectMinervaJobTest.java b/service/src/test/java/lcsb/mapviewer/services/jobs/AnnotateProjectMinervaJobTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..0eaeba78fe6c8a441c1f1a68ae96e872a3f15c4a
--- /dev/null
+++ b/service/src/test/java/lcsb/mapviewer/services/jobs/AnnotateProjectMinervaJobTest.java
@@ -0,0 +1,40 @@
+package lcsb.mapviewer.services.jobs;
+
+import lcsb.mapviewer.annotation.services.ModelAnnotator;
+import lcsb.mapviewer.model.user.UserAnnotationSchema;
+import lcsb.mapviewer.services.ServiceTestFunctions;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.beans.factory.annotation.Autowired;
+
+import static org.junit.Assert.assertEquals;
+
+public class AnnotateProjectMinervaJobTest extends ServiceTestFunctions {
+
+  @Autowired
+  private ModelAnnotator modelAnnotator;
+
+  @Before
+  public void setUp() throws Exception {
+  }
+
+  @After
+  public void tearDown() throws Exception {
+  }
+
+  @Test
+  public void testJobSerialization() {
+    UserAnnotationSchema schema = modelAnnotator.createDefaultAnnotatorSchema();
+    AnnotateProjectMinervaJob job = new AnnotateProjectMinervaJob("test", schema);
+
+    AnnotateProjectMinervaJob job2 = new AnnotateProjectMinervaJob("test", new UserAnnotationSchema());
+
+    job2.assignData(job.getJobParameters());
+
+    UserAnnotationSchema serializedSchema = job2.getSchema();
+
+    assertEquals(schema.getClassAnnotators().size(), serializedSchema.getClassAnnotators().size());
+    assertEquals(job.getJobParameters(), job2.getJobParameters());
+  }
+}
diff --git a/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectJobController.java b/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectJobController.java
new file mode 100644
index 0000000000000000000000000000000000000000..bf9087eb7775120c9e138337818b5f5b895f74cb
--- /dev/null
+++ b/web/src/main/java/lcsb/mapviewer/web/api/project/NewProjectJobController.java
@@ -0,0 +1,73 @@
+package lcsb.mapviewer.web.api.project;
+
+import lcsb.mapviewer.model.Project;
+import lcsb.mapviewer.model.job.MinervaJob;
+import lcsb.mapviewer.persist.dao.MinervaJobProperty;
+import lcsb.mapviewer.services.FailedDependencyException;
+import lcsb.mapviewer.services.ObjectNotFoundException;
+import lcsb.mapviewer.services.interfaces.IMinervaJobService;
+import lcsb.mapviewer.services.interfaces.IProjectService;
+import lcsb.mapviewer.web.api.NewApiResponseSerializer;
+import org.hibernate.validator.constraints.NotBlank;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.data.domain.Pageable;
+import org.springframework.http.MediaType;
+import org.springframework.http.ResponseEntity;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Map;
+
+@RestController
+@Validated
+@RequestMapping(
+    value = {
+        "/minerva/new_api/projects/{projectId:.+}/job",
+    },
+    produces = MediaType.APPLICATION_JSON_VALUE)
+public class NewProjectJobController {
+
+  private final IProjectService projectService;
+  private final IMinervaJobService minervaJobService;
+  private final NewApiResponseSerializer serializer;
+
+  @Autowired
+  public NewProjectJobController(
+      final IProjectService projectService,
+      final NewApiResponseSerializer serializer,
+      final IMinervaJobService minervaJobService
+  ) {
+    this.projectService = projectService;
+    this.serializer = serializer;
+    this.minervaJobService = minervaJobService;
+  }
+
+  @PreAuthorize("hasAuthority('IS_ADMIN')"
+      + " or hasAuthority('IS_CURATOR') and hasAuthority('READ_PROJECT:' + #projectId)")
+  @GetMapping
+  public ResponseEntity<?> getLogEntries(
+      final @NotBlank @PathVariable(value = "projectId") String projectId,
+      final Pageable pageable)
+      throws ObjectNotFoundException, FailedDependencyException, IOException {
+    Project project = projectService.getProjectByProjectId(projectId);
+    if (project == null) {
+      throw new ObjectNotFoundException();
+    }
+    Map<MinervaJobProperty, Object> searchFilter = new HashMap<>();
+    searchFilter.put(MinervaJobProperty.EXTERNAL_OBJECT_CLASS, Collections.singleton(Project.class));
+    searchFilter.put(MinervaJobProperty.EXTERNAL_OBJECT_ID, Collections.singleton(project.getId()));
+
+    Page<MinervaJob> page = minervaJobService.getAll(searchFilter, pageable);
+
+    return serializer.prepareResponse(page);
+
+  }
+}
\ No newline at end of file
diff --git a/web/src/main/java/lcsb/mapviewer/web/api/user/NewUserController.java b/web/src/main/java/lcsb/mapviewer/web/api/user/NewUserController.java
index f6555a3e5ee7b18488dc0a78bc7740b56b223d70..abefda97ed55655b347da82eae753d350abf9d23 100644
--- a/web/src/main/java/lcsb/mapviewer/web/api/user/NewUserController.java
+++ b/web/src/main/java/lcsb/mapviewer/web/api/user/NewUserController.java
@@ -18,9 +18,9 @@ import lcsb.mapviewer.persist.dao.user.UserProperty;
 import lcsb.mapviewer.services.InvalidTokenException;
 import lcsb.mapviewer.services.ObjectExistsException;
 import lcsb.mapviewer.services.ObjectNotFoundException;
+import lcsb.mapviewer.services.interfaces.IAnnotationService;
 import lcsb.mapviewer.services.interfaces.IConfigurationService;
 import lcsb.mapviewer.services.interfaces.ILdapService;
-import lcsb.mapviewer.services.interfaces.IProjectService;
 import lcsb.mapviewer.services.interfaces.IUserService;
 import lcsb.mapviewer.services.utils.EmailException;
 import lcsb.mapviewer.services.utils.EmailSender;
@@ -71,7 +71,7 @@ public class NewUserController extends BaseController {
 
   private final IUserService userService;
   private final ILdapService ldapService;
-  private final IProjectService projectService;
+  private final IAnnotationService annotationService;
   private final IConfigurationService configurationService;
   private final PasswordEncoder passwordEncoder;
   private final EmailSender emailSender;
@@ -86,16 +86,16 @@ public class NewUserController extends BaseController {
                            final EmailSender emailSender,
                            final IConfigurationService configurationService,
                            final ILdapService ldapService,
-                           final IProjectService projectService,
+                           final IAnnotationService annotationService,
                            final SessionRegistry sessionRegistry,
                            final NewApiResponseSerializer serializer) {
     this.userService = userService;
     this.passwordEncoder = passwordEncoder;
     this.emailSender = emailSender;
-    this.projectService = projectService;
     this.configurationService = configurationService;
     this.serializer = serializer;
     this.ldapService = ldapService;
+    this.annotationService = annotationService;
     this.userResponseDecorator = new NewUserResponseDecorator(ldapUsernames, sessionRegistry);
     this.refreshLdapUsernames();
   }
@@ -128,7 +128,7 @@ public class NewUserController extends BaseController {
       throw new ObjectNotFoundException("User doesn't exist");
     }
     if (user.getAnnotationSchema() == null) {
-      user.setAnnotationSchema(projectService.prepareUserAnnotationSchema(user, true));
+      user.setAnnotationSchema(annotationService.prepareUserAnnotationSchema(user, true));
     }
     return serializer.prepareResponse(user, this.userResponseDecorator);
   }
diff --git a/web/src/test/java/lcsb/mapviewer/web/ControllerIntegrationTest.java b/web/src/test/java/lcsb/mapviewer/web/ControllerIntegrationTest.java
index 4443155aeb282b3024f0ed6e1fb87fffb15b9778..19c48cc114adc7c47e18a2d6a480ac71e8ee9ca5 100644
--- a/web/src/test/java/lcsb/mapviewer/web/ControllerIntegrationTest.java
+++ b/web/src/test/java/lcsb/mapviewer/web/ControllerIntegrationTest.java
@@ -2,6 +2,7 @@ package lcsb.mapviewer.web;
 
 import com.fasterxml.jackson.core.type.TypeReference;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import lcsb.mapviewer.annotation.cache.CacheQueryMinervaJob;
 import lcsb.mapviewer.annotation.data.serializer.ChemicalSerializer;
 import lcsb.mapviewer.annotation.services.PubmedSearchException;
 import lcsb.mapviewer.common.TextFileUtils;
@@ -20,6 +21,9 @@ import lcsb.mapviewer.model.graphics.HorizontalAlign;
 import lcsb.mapviewer.model.graphics.LineType;
 import lcsb.mapviewer.model.graphics.PolylineData;
 import lcsb.mapviewer.model.graphics.VerticalAlign;
+import lcsb.mapviewer.model.job.MinervaJob;
+import lcsb.mapviewer.model.job.MinervaJobPriority;
+import lcsb.mapviewer.model.job.MinervaJobType;
 import lcsb.mapviewer.model.map.Comment;
 import lcsb.mapviewer.model.map.MiriamData;
 import lcsb.mapviewer.model.map.MiriamType;
@@ -1035,4 +1039,11 @@ public abstract class ControllerIntegrationTest extends TestUtils {
     return result;
   }
 
+  protected MinervaJob createMinervaJob() {
+    return new MinervaJob(
+        MinervaJobType.REFRESH_CACHE,
+        MinervaJobPriority.LOWEST,
+        new CacheQueryMinervaJob("https://google.lu", -1));
+  }
+
 }
diff --git a/web/src/test/java/lcsb/mapviewer/web/ProjectControllerIntegrationTest.java b/web/src/test/java/lcsb/mapviewer/web/ProjectControllerIntegrationTest.java
index 41176a4ddac27fe0c0ff93e6cadfe4ff21b7e42c..29819fe5e649b8e86f2b6fb61ca03bc723b7571b 100644
--- a/web/src/test/java/lcsb/mapviewer/web/ProjectControllerIntegrationTest.java
+++ b/web/src/test/java/lcsb/mapviewer/web/ProjectControllerIntegrationTest.java
@@ -1403,6 +1403,8 @@ public class ProjectControllerIntegrationTest extends ControllerIntegrationTest
     project.removeProjectBackground(project.getProjectBackgrounds().get(0));
     projectService.update(project);
 
+    minervaJobService.waitForTasksToFinish();
+
     final String projectHomeDir = projectService.getProjectHomeDir(project);
 
     new File(projectHomeDir).mkdirs();
diff --git a/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectDiseaseControllerTest.java b/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectDiseaseControllerTest.java
index 82115c106f112e87700899bbf5da8d6cf73e8fbd..5883cab8a302b5fc485e0465d6b1e2e812c33026 100644
--- a/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectDiseaseControllerTest.java
+++ b/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectDiseaseControllerTest.java
@@ -93,6 +93,8 @@ public class NewProjectDiseaseControllerTest extends ControllerIntegrationTest {
     project.setDisease(null);
     projectService.update(project);
 
+    minervaJobService.waitForTasksToFinish();
+
     NewMiriamDataDTO data = createMiriamDataDTO(MiriamType.MESH_2012, "D010300");
 
     RequestBuilder request = post("/minerva/new_api/projects/{projectId}/disease", TEST_PROJECT)
diff --git a/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectJobControllerTest.java b/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectJobControllerTest.java
new file mode 100644
index 0000000000000000000000000000000000000000..bb4cf8a6f5a77993c0d19fd09b614de0c4ec9f6d
--- /dev/null
+++ b/web/src/test/java/lcsb/mapviewer/web/api/project/NewProjectJobControllerTest.java
@@ -0,0 +1,70 @@
+package lcsb.mapviewer.web.api.project;
+
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lcsb.mapviewer.model.Project;
+import lcsb.mapviewer.model.job.MinervaJob;
+import lcsb.mapviewer.services.interfaces.IMinervaJobService;
+import lcsb.mapviewer.web.ControllerIntegrationTest;
+import lcsb.mapviewer.web.api.NewApiResponseSerializer;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.data.domain.Page;
+import org.springframework.mock.web.MockHttpSession;
+import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
+import org.springframework.test.web.servlet.RequestBuilder;
+
+import static org.junit.Assert.assertEquals;
+import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get;
+import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
+
+@RunWith(SpringJUnit4ClassRunner.class)
+public class NewProjectJobControllerTest extends ControllerIntegrationTest {
+
+  @Autowired
+  private IMinervaJobService minervaJobService;
+
+  @Autowired
+  private NewApiResponseSerializer newApiResponseSerializer;
+
+  private ObjectMapper objectMapper;
+
+  @Before
+  public void setUp() throws Exception {
+    objectMapper = newApiResponseSerializer.getObjectMapper();
+  }
+
+  @After
+  public void tearDown() throws Exception {
+    minervaJobService.waitForTasksToFinish();
+    removeProject(TEST_PROJECT);
+  }
+
+  @Test
+  public void testGetJobsForProject() throws Exception {
+    Project project = createAndPersistProject(TEST_PROJECT);
+
+    MinervaJob job = createMinervaJob();
+    job.setExternalObject(project);
+    minervaJobService.add(job);
+
+    MinervaJob job2 = createMinervaJob();
+    minervaJobService.add(job2);
+
+    MockHttpSession session = createSession(BUILT_IN_TEST_ADMIN_LOGIN, BUILT_IN_TEST_ADMIN_PASSWORD);
+
+    RequestBuilder request = get(
+        "/minerva/new_api/projects/{projectId}/job/",
+        TEST_PROJECT).session(session);
+
+    String response = mockMvc.perform(request)
+        .andExpect(status().is2xxSuccessful())
+        .andReturn().getResponse().getContentAsString();
+    Page<Object> result = objectMapper.readValue(response, new TypeReference<Page<Object>>() {
+    });
+    assertEquals(1, result.getTotalElements());
+  }
+}