diff --git a/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java
index b64acf509685048b6b0036fbf77d20ea19a9363a..91843299a881659f96bc79a9d4938c7d9a7735df 100644
--- a/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java
+++ b/src/main/java/org/caosdb/server/jobs/core/CheckStateTransition.java
@@ -1,5 +1,6 @@
 package org.caosdb.server.jobs.core;
 
+import org.apache.shiro.authz.AuthorizationException;
 import org.caosdb.server.entity.DeleteEntity;
 import org.caosdb.server.entity.Message;
 import org.caosdb.server.entity.Message.MessageType;
@@ -7,6 +8,7 @@ import org.caosdb.server.entity.UpdateEntity;
 import org.caosdb.server.jobs.JobAnnotation;
 import org.caosdb.server.jobs.JobExecutionTime;
 import org.caosdb.server.transaction.WriteTransaction;
+import org.caosdb.server.utils.ServerMessages;
 
 /**
  * Check if the attempted state transition is allowed.
@@ -46,6 +48,9 @@ public class CheckStateTransition extends EntityStateJob {
       }
     } catch (Message m) {
       getEntity().addError(m);
+    } catch (AuthorizationException e) {
+      getEntity().addError(ServerMessages.AUTHORIZATION_ERROR);
+      getEntity().addInfo(e.getMessage());
     }
   }
 
@@ -88,12 +93,20 @@ public class CheckStateTransition extends EntityStateJob {
       return;
     }
 
+    boolean transition_defined = false;
     for (Transition t : stateModel.getTransitions()) {
       if (t.isFromState(oldState) && t.isToState(newState)) {
-        // TODO permissions
-        return;
+        transition_defined = true;
+        if (t.isPermitted(getUser())) {
+          return;
+        }
       }
     }
+    if (transition_defined) {
+      throw new AuthorizationException(
+          getUser().getPrincipal().toString()
+              + " doesn't have permission to perform this transition.");
+    }
     throw TRANSITION_NOT_ALLOWED;
   }
 
diff --git a/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java
index 39f4d6754ed665a0590c7a02f1e1882b8a907b64..a18abd11131fef7e184e318f94860ff1bf72bc5c 100644
--- a/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java
+++ b/src/main/java/org/caosdb/server/jobs/core/EntityStateJob.java
@@ -1,11 +1,15 @@
 package org.caosdb.server.jobs.core;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.HashSet;
 import java.util.Iterator;
+import java.util.LinkedList;
 import java.util.List;
+import java.util.Map;
 import java.util.Objects;
 import java.util.Set;
+import org.apache.shiro.subject.Subject;
 import org.caosdb.server.database.exceptions.EntityDoesNotExistException;
 import org.caosdb.server.datatype.AbstractCollectionDatatype;
 import org.caosdb.server.datatype.CollectionValue;
@@ -21,6 +25,8 @@ import org.caosdb.server.entity.StatementStatus;
 import org.caosdb.server.entity.wrapper.Property;
 import org.caosdb.server.entity.xml.ToElementable;
 import org.caosdb.server.jobs.EntityJob;
+import org.caosdb.server.permissions.EntityACI;
+import org.caosdb.server.permissions.EntityACL;
 import org.caosdb.server.query.Query;
 import org.caosdb.server.utils.EntityStatus;
 import org.jdom2.Element;
@@ -58,9 +64,16 @@ public abstract class EntityStateJob extends EntityJob {
   public static final String STATE_RECORD_TYPE_NAME = "State";
   public static final String STATE_MODEL_RECORD_TYPE_NAME = "StateModel";
   public static final String TRANSITION_RECORD_TYPE_NAME = "Transition";
+  public static final String TRANSITION_XML_TAG = "Transition";
+  public static final String TRANSITION_ATTRIBUTE_NAME = "name";
+  public static final String TRANSITION_ATTRIBUTE_DESCRIPTION = "description";
+  public static final String TO_XML_TAG = "ToState";
+  public static final String FROM_XML_TAG = "FromState";
   public static final String STATE_XML_TAG = "State";
-  public static final String MODEL_ATTRIBUTE_NAME = "model";
+  public static final String STATE_ATTRIBUTE_MODEL = "model";
   public static final String STATE_ATTRIBUTE_NAME = "name";
+  public static final String STATE_ATTRIBUTE_DESCRIPTION = "description";
+  public static final String STATE_ATTRIBUTE_ID = "id";
 
   public static final Message STATE_MODEL_NOT_FOUND =
       new Message(MessageType.Error, "StateModel not found.");
@@ -86,12 +99,14 @@ public abstract class EntityStateJob extends EntityJob {
    */
   public class Transition {
 
+    private String name;
+    private String description;
     private State fromState;
     private State toState;
-    private String name;
 
     public Transition(EntityInterface transition) throws Message {
       this.name = transition.getName();
+      this.description = transition.getDescription();
       this.fromState = getFromState(transition);
       this.toState = getToState(transition);
     }
@@ -153,6 +168,10 @@ public abstract class EntityStateJob extends EntityJob {
       return this.name;
     }
 
+    public String getDescription() {
+      return this.description;
+    }
+
     @Override
     public String toString() {
       return "Transition (name="
@@ -163,6 +182,22 @@ public abstract class EntityStateJob extends EntityJob {
           + getToState().getStateName()
           + ")";
     }
+
+    public Element toElement() {
+      Element result = new Element(TRANSITION_XML_TAG);
+      if (this.name != null) result.setAttribute(TRANSITION_ATTRIBUTE_NAME, this.name);
+      if (this.description != null)
+        result.setAttribute(TRANSITION_ATTRIBUTE_DESCRIPTION, this.description);
+      Element to = new Element(TO_XML_TAG);
+      to.setAttribute(STATE_ATTRIBUTE_NAME, this.toState.stateName);
+      Element from = new Element(FROM_XML_TAG);
+      from.setAttribute(STATE_ATTRIBUTE_NAME, this.fromState.stateName);
+      return result.addContent(from).addContent(to);
+    }
+
+    public boolean isPermitted(Subject user) {
+      return user.isPermitted("STATE:TRANSITION:" + this.name);
+    }
   }
 
   /**
@@ -191,6 +226,9 @@ public abstract class EntityStateJob extends EntityJob {
     private EntityInterface stateEntity = null;
     private EntityInterface stateModelEntity = null;
     private StateModel stateModel;
+    private String stateDescription = null;
+    private Integer stateId = null;
+    private EntityACL stateACL = null;
 
     public State(String stateName, String stateModelName) throws Message {
       this.stateName = stateName;
@@ -199,9 +237,37 @@ public abstract class EntityStateJob extends EntityJob {
 
     public State(EntityInterface stateEntity, EntityInterface stateModelEntity) throws Message {
       this.stateEntity = stateEntity;
+      this.stateDescription = stateEntity.getDescription();
+      this.stateId = stateEntity.getId();
       this.stateName = stateEntity.getName();
       this.stateModelEntity = stateModelEntity;
       this.stateModelName = stateModelEntity.getName();
+      this.stateACL = createStateACL(stateEntity.getEntityACL());
+    }
+
+    private EntityACL createStateACL(EntityACL entityACL) {
+      LinkedList<EntityACI> rules = new LinkedList<>();
+      for (EntityACI aci : entityACL.getRules()) {
+        if (aci.getResponsibleAgent().toString().startsWith("?STATE?")) {
+          int end = aci.getResponsibleAgent().toString().length() - 1;
+          String role = aci.getResponsibleAgent().toString().substring(7, end);
+          rules.add(
+              new EntityACI(org.caosdb.server.permissions.Role.create(role), aci.getBitSet()));
+        }
+      }
+      return new EntityACL(rules);
+    }
+
+    public EntityACL getStateACL() {
+      return this.stateACL;
+    }
+
+    public String getStateDescription() throws Message {
+      return this.stateDescription;
+    }
+
+    public Integer getStateId() throws Message {
+      return this.stateId;
     }
 
     @Override
@@ -223,11 +289,31 @@ public abstract class EntityStateJob extends EntityJob {
     @Override
     public void addToElement(Element ret) {
       Element e = new Element(STATE_XML_TAG);
-      if (this.getStateModelName() != null) {
-        e.setAttribute(MODEL_ATTRIBUTE_NAME, this.getStateModelName());
+      if (this.stateModelName != null) {
+        e.setAttribute(STATE_ATTRIBUTE_MODEL, this.stateModelName);
+      }
+      if (this.stateName != null) {
+        e.setAttribute(STATE_ATTRIBUTE_NAME, this.stateName);
+      }
+      if (this.stateDescription != null) {
+        e.setAttribute(STATE_ATTRIBUTE_DESCRIPTION, this.stateDescription);
+      }
+      if (this.stateId != null) {
+        e.setAttribute(STATE_ATTRIBUTE_ID, Integer.toString(this.stateId));
       }
-      if (this.getStateName() != null) {
-        e.setAttribute(STATE_ATTRIBUTE_NAME, this.getStateName());
+      if (this.stateModelEntity != null) {
+        try {
+          this.getStateModel()
+              .transitions
+              .forEach(
+                  (Transition t) -> {
+                    if (t.isFromState(this) && t.isPermitted(getUser())) {
+                      e.addContent(t.toElement());
+                    }
+                  });
+        } catch (Message m) {
+          getEntity().addError(m);
+        }
       }
       ret.addContent(e);
     }
@@ -281,6 +367,9 @@ public abstract class EntityStateJob extends EntityJob {
     public EntityInterface getStateEntity() throws Message {
       if (this.stateEntity == null) {
         this.stateEntity = retrieveStateEntity(this.getStateName());
+        this.stateDescription = this.stateEntity.getDescription();
+        this.stateId = this.stateEntity.getId();
+        this.stateACL = createStateACL(this.stateEntity.getEntityACL());
       }
       return this.stateEntity;
     }
@@ -321,16 +410,11 @@ public abstract class EntityStateJob extends EntityJob {
     private State finalState;
 
     public StateModel(EntityInterface stateModelEntity) throws Message {
-      long time1 = System.currentTimeMillis();
       this.name = stateModelEntity.getName();
       this.transitions = getTransitions(stateModelEntity);
       this.states = getStates(transitions, this);
       this.finalState = getFinalState(stateModelEntity);
       this.initialState = getInitialState(stateModelEntity);
-      long time2 = System.currentTimeMillis();
-      getTransaction()
-          .getTransactionBenchmark()
-          .addMeasurement(this.getClass().getSimpleName() + "::1", time2 - time1);
     }
 
     private State getInitialState(EntityInterface stateModelEntity) throws Message {
@@ -546,7 +630,7 @@ public abstract class EntityStateJob extends EntityJob {
   }
 
   protected State createState(ClientMessage s) throws Message {
-    String stateModel = s.getProperty(MODEL_ATTRIBUTE_NAME);
+    String stateModel = s.getProperty(STATE_ATTRIBUTE_MODEL);
     if (stateModel == null) {
       throw STATE_MODEL_NOT_SPECIFIED;
     }
@@ -570,7 +654,12 @@ public abstract class EntityStateJob extends EntityJob {
     try {
       p.parseValue();
       ReferenceValue refid = (ReferenceValue) p.getValue();
-      EntityInterface stateEntity = retrieveValidSparseEntityById(refid.getId(), null);
+      EntityInterface stateEntity = cache.get("state" + Integer.toString(refid.getId()));
+      boolean cached = true;
+      if (stateEntity == null || !cached) {
+        stateEntity = retrieveValidSparseEntityById(refid.getId(), null);
+        cache.put("state" + Integer.toString(refid.getId()), stateEntity);
+      }
 
       EntityInterface stateModelEntity = findStateModel(stateEntity);
       return new State(stateEntity, stateModelEntity);
@@ -581,8 +670,16 @@ public abstract class EntityStateJob extends EntityJob {
     }
   }
 
+  private static final Map<String, EntityInterface> cache = new HashMap<>();
+
   EntityInterface findStateModel(EntityInterface stateEntity) throws Exception {
     // TODO this should be cached
+
+    boolean cached = true;
+    EntityInterface result = cache.get("modelof" + Integer.toString(stateEntity.getId()));
+    if (result != null && cached) {
+      return result;
+    }
     Query query =
         new Query(
             "FIND RECORD "
@@ -594,6 +691,8 @@ public abstract class EntityStateJob extends EntityJob {
             getUser(),
             null);
     query.execute(getTransaction().getAccess());
-    return retrieveValidEntity(query.getResultSet().get(0));
+    result = retrieveValidEntity(query.getResultSet().get(0));
+    cache.put("modelof" + Integer.toString(stateEntity.getId()), result);
+    return result;
   }
 }
diff --git a/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java b/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java
index 8bb1e00ac8de9c4dddab2c7a770c01ba19568572..e4a32bc5f8f2bd3b7e11363f07992a9ceb3b16ef 100644
--- a/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java
+++ b/src/main/java/org/caosdb/server/jobs/core/InitEntityStateJobs.java
@@ -67,6 +67,7 @@ public class InitEntityStateJobs extends EntityStateJob implements Observer {
         oldState = null;
         if (states.size() == 1) {
           oldState = states.get(0);
+          ((UpdateEntity) getEntity()).getOriginal().setEntityACL(getEntity().getEntityACL());
         }
         if (!Objects.equals(newState, oldState)) {
           getEntity().acceptObserver(this);
@@ -92,13 +93,20 @@ public class InitEntityStateJobs extends EntityStateJob implements Observer {
             MessageType.Error, "Currently, each entity can only have one state at a time.");
       } else if (states.size() == 1) {
         newState = states.get(0);
+        transferEntityACL(getEntity(), newState);
       }
     } catch (Message m) {
       getEntity().addError(m);
     }
+
     return newState;
   }
 
+  private void transferEntityACL(EntityInterface entity, State newState) throws Message {
+    newState.getStateEntity();
+    entity.setEntityACL(newState.getStateACL());
+  }
+
   private static final Message STATE_ERROR_IN_ORIGINAL_ENTITY(Message m) {
     return new Message(
         MessageType.Warning, "State error in previous entity version\n" + m.getDescription());
diff --git a/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java b/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java
index 38fd239844df96cd94025225600964fb9e1bdd74..f2090d8245bc07bb40965562c93adc4ee71185c8 100644
--- a/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java
+++ b/src/main/java/org/caosdb/server/jobs/core/MakeStateMessage.java
@@ -4,9 +4,11 @@ import java.util.List;
 import org.caosdb.server.CaosDBServer;
 import org.caosdb.server.entity.Message;
 import org.caosdb.server.entity.wrapper.Property;
+import org.caosdb.server.entity.xml.ToElementable;
 import org.caosdb.server.jobs.JobAnnotation;
 import org.caosdb.server.jobs.JobExecutionTime;
 import org.caosdb.server.transaction.Retrieve;
+import org.jdom2.Element;
 
 /**
  * Remove the state property from the entity and, iff necessary, convert it into a State instance
@@ -23,6 +25,8 @@ import org.caosdb.server.transaction.Retrieve;
     time = JobExecutionTime.POST_TRANSACTION)
 public class MakeStateMessage extends EntityStateJob {
 
+  public static final String SPARSE_FLAG = "sparseState";
+
   @Override
   protected void run() {
 
@@ -33,9 +37,27 @@ public class MakeStateMessage extends EntityStateJob {
 
         // only add the State during Retrieve. In all other cases, the State is already present.
         if (stateProperties != null && getTransaction() instanceof Retrieve) {
-          for (Property s : stateProperties) {
-            State stateMessage = createState(s);
-            getEntity().addMessage(stateMessage);
+          String sparse =
+              getTransaction().getContainer().getFlags().getOrDefault(SPARSE_FLAG, "false");
+          if ("true".equals(sparse)) {
+            for (Property s : stateProperties) {
+              getEntity()
+                  .addMessage(
+                      new ToElementable() {
+                        @Override
+                        public void addToElement(Element ret) {
+                          Element state = new Element(STATE_XML_TAG);
+                          state.setAttribute(STATE_ATTRIBUTE_ID, s.getValue().toString());
+                          ret.addContent(state);
+                        }
+                      });
+            }
+          } else {
+            for (Property s : stateProperties) {
+              State stateMessage = createState(s);
+              stateMessage.getStateModel();
+              getEntity().addMessage(stateMessage);
+            }
           }
         }
       } catch (Message e) {