diff --git a/src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java
new file mode 100644
index 0000000000000000000000000000000000000000..9840a908dccff3155f892237b9f6b8c093c67136
--- /dev/null
+++ b/src/main/java/caosdb/server/jobs/extension/AWIBoxLoan.java
@@ -0,0 +1,308 @@
+package caosdb.server.jobs.extension;
+
+import caosdb.server.CaosDBServer;
+import caosdb.server.accessControl.UserSources;
+import caosdb.server.entity.Entity;
+import caosdb.server.entity.EntityInterface;
+import caosdb.server.entity.Message;
+import caosdb.server.entity.wrapper.Property;
+import caosdb.server.jobs.ContainerJob;
+import caosdb.server.jobs.JobAnnotation;
+import caosdb.server.jobs.core.CheckNoAdditionalPropertiesPresent;
+import caosdb.server.jobs.core.CheckNoOverridesPresent;
+import caosdb.server.jobs.core.CheckPropValid;
+import caosdb.server.query.Query;
+import caosdb.server.transaction.Insert;
+import caosdb.server.transaction.Update;
+import caosdb.server.utils.EntityStatus;
+import caosdb.server.utils.ServerMessages;
+import caosdb.server.utils.Utils;
+import java.util.List;
+
+@JobAnnotation(transaction = caosdb.server.transaction.WriteTransaction.class, loadAlways = true)
+public class AWIBoxLoan extends ContainerJob {
+
+  private static final Message UNIQUE_USER =
+      new Message("The user must have a unique combination of first name and last name!");
+  private static final Message BOX_HAS_LOAN =
+      new Message(
+          "This box cannot be be requested right now because it appears to have a Loan property attached to it. This usually means, that the box is already requested or borrowed by someone.");
+
+  @Override
+  protected void run() {
+    if (isAnonymous()
+        && !(isRequestLoanSetUser()
+            || isRequestLoanInsertLoan()
+            || isRequestLoanUpdateBox()
+            || isRequestReturnSetUser()
+            || isRequestReturnUpdateLoan())) {
+      addError(ServerMessages.AUTHORIZATION_ERROR);
+    }
+  }
+
+  boolean isAnonymous() {
+    return getUser().hasRole(UserSources.ANONYMOUS_ROLE);
+  }
+
+  boolean isRequestReturnUpdateLoan() {
+    // is UPDATE transaction
+    if (getTransaction() instanceof Update) {
+      // Container has only loan elements with special properties
+      for (EntityInterface e : getContainer()) {
+        if (!isLoan(e) || !hasOnlyAllowedLoanProperties4RequestReturn(e)) {
+          return false;
+        }
+        setReturnRequestedDate(e);
+      }
+      return true;
+    }
+    return false;
+  }
+
+  boolean isRequestReturnSetUser() {
+    // same as request_loan.set_user
+    return isRequestLoanSetUser();
+  }
+
+  boolean isRequestLoanUpdateBox() {
+    // is UPDATE transaction
+    if (getTransaction() instanceof Update) {
+      // Container has only box elements
+      for (EntityInterface e : getContainer()) {
+        if (boxHasLoanProperty(e)) {
+          e.addError(BOX_HAS_LOAN);
+          return true;
+        }
+        if (!isBox(e) || !hasOnlyAllowedBoxProperties4RequestLoan(e)) {
+          return false;
+        }
+      }
+      return true;
+    }
+    return false;
+  }
+
+  private boolean boxHasLoanProperty(EntityInterface e) {
+    EntityInterface validBox = retrieveValidEntity(e.getId());
+    for (Property p : validBox.getProperties()) {
+      if (p.getId() == getLoanId()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  /**
+   * Has only one new property -> Loan.
+   *
+   * @param e
+   * @return
+   */
+  boolean hasOnlyAllowedBoxProperties4RequestLoan(EntityInterface e) {
+    int count = 0;
+    for (Property p : e.getProperties()) {
+      if (p.getEntityStatus() == EntityStatus.QUALIFIED) { // this means update
+        if (p.getId() == getLoanId()) {
+          count++;
+          continue;
+        }
+        return false; // this is not a Loan.
+      }
+    }
+
+    // Box has only one update, a loan property
+    return count == 1;
+  }
+
+  boolean isRequestLoanInsertLoan() {
+    // is INSERT transaction
+    if (getTransaction() instanceof Insert) {
+      // Container has only loan elements
+      for (EntityInterface e : getContainer()) {
+        if (!isLoan(e)) {
+          return false;
+        }
+        setLoanRequestDate(e);
+        // Loans have only a specific set of properties
+        appendJob(e, CheckNoAdditionalPropertiesPresent.class);
+        appendJob(e, CheckNoOverridesPresent.class);
+      }
+      return true;
+    }
+    return false;
+  }
+
+  void setReturnRequestedDate(EntityInterface e) {
+    setDateProperty(e, getReturnRequestedId());
+  }
+
+  private void setDateProperty(EntityInterface e, Integer propertyId) {
+    EntityInterface p = retrieveValidEntity(propertyId);
+    p.setValue(getTransaction().getTimestamp());
+    e.addProperty(new Property(p));
+  }
+
+  void setLoanRequestDate(EntityInterface e) {
+    setDateProperty(e, getLoanRequestedId());
+  }
+
+  boolean isRequestLoanSetUser() {
+    // is INSERT/UPDATE transaction
+    // Container has only one element, user
+    if ((getTransaction() instanceof Update || getTransaction() instanceof Insert)
+        && getContainer().size() == 1
+        && isUser(getContainer().get(0))
+        && checkUniqueName(getContainer().get(0))
+        && checkEmail(getContainer().get(0))) {
+      appendJob(getContainer().get(0), CheckNoAdditionalPropertiesPresent.class);
+      appendJob(getContainer().get(0), CheckNoOverridesPresent.class);
+      return true;
+    }
+    return false;
+  }
+
+  boolean checkEmail(Entity entity) {
+    runJobFromSchedule(entity, CheckPropValid.class);
+    for (Property p : entity.getProperties()) {
+      if (p.getId() == getEmailID()) {
+        if (!Utils.isRFC822Compliant(p.getValue().toString())) {
+          p.addError(ServerMessages.EMAIL_NOT_WELL_FORMED);
+        }
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private boolean checkUniqueName(Entity entity) {
+    String firstName = null;
+    String lastName = null;
+    Query q =
+        new Query(
+                "FIND "
+                    + getUserID().toString()
+                    + " WITH "
+                    + getFirstNameId().toString()
+                    + "='"
+                    + firstName
+                    + "' AND "
+                    + getLastNameId().toString()
+                    + "='"
+                    + lastName
+                    + "'",
+                getUser())
+            .execute(getTransaction().getAccess());
+    List<Integer> resultSet = q.getResultSet();
+    if (resultSet.isEmpty() || (resultSet.size() == 1 && resultSet.get(0) == entity.getId())) {
+      return true;
+    }
+    entity.addError(UNIQUE_USER);
+    return false;
+  }
+
+  /** Has single user parent. */
+  boolean isUser(Entity entity) {
+    return entity.getParents().size() == 1
+        && retrieveValidIDByName(entity.getParents().get(0).getName()) == getUserID();
+  }
+
+  /** Has single box parent. */
+  boolean isBox(EntityInterface e) {
+    return e.getParents().size() == 1
+        && retrieveValidIDByName(e.getParents().get(0).getName()) == getBoxId();
+  }
+
+  /** has single loan parent */
+  private boolean isLoan(EntityInterface e) {
+    return e.getParents().size() == 1
+        && retrieveValidIDByName(e.getParents().get(0).getName()) == getLoanId();
+  }
+  /**
+   * Has only 5/6 new/updated properties: content, returnRequested, destination, Borrower, comment
+   * (optional), location
+   */
+  boolean hasOnlyAllowedLoanProperties4RequestReturn(EntityInterface e) {
+    runJobFromSchedule(e, CheckPropValid.class);
+    for (Property p : e.getProperties()) {
+      if (p.getEntityStatus() == EntityStatus.QUALIFIED) { // this means update
+        if (p.getId() == getContentId()) {
+
+        } else if (p.getId() == getDestinationId()) {
+
+        } else if (p.getId() == getBorrowerId()) {
+
+        } else if (p.getId() == getCommentId()) {
+
+        } else if (p.getId() == getLocationId()) {
+
+        }
+        return false; // this is not a property which may be updated by anonymous.
+      }
+    }
+    return true;
+  }
+
+  Integer getIdOf(String string) {
+    String id = CaosDBServer.getServerProperty("EXT_AWI_" + string.toUpperCase() + "_ID");
+    if (id != null && Utils.isNonNullInteger(id)) {
+      return new Integer(id);
+    }
+    String name = CaosDBServer.getServerProperty("EXT_AWI_" + string.toUpperCase() + "_NAME");
+    if (name == null || name.isEmpty()) {
+      name = string;
+    }
+    return retrieveValidIDByName(name);
+  }
+
+  Integer getBorrowerId() {
+    return getIdOf("Borrower");
+  }
+
+  Integer getCommentId() {
+    return getIdOf("comment");
+  }
+
+  Integer getLocationId() {
+    return getIdOf("location");
+  }
+
+  Integer getDestinationId() {
+    return getIdOf("destination");
+  }
+
+  Integer getContentId() {
+    return getIdOf("content");
+  }
+
+  Integer getBoxId() {
+    return getIdOf("Box");
+  }
+
+  Integer getLoanId() {
+    return getIdOf("Loan");
+  }
+
+  Integer getUserID() {
+    return getIdOf("User");
+  }
+
+  Integer getEmailID() {
+    return getIdOf("email");
+  }
+
+  Integer getLoanRequestedId() {
+    return getIdOf("loanRequested");
+  }
+
+  Integer getReturnRequestedId() {
+    return getIdOf("returnRequested");
+  }
+
+  Integer getLastNameId() {
+    return getIdOf("lastName");
+  }
+
+  Integer getFirstNameId() {
+    return getIdOf("firstName");
+  }
+}
diff --git a/src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java b/src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java
new file mode 100644
index 0000000000000000000000000000000000000000..59ce687d2d82be5d725ebdff1eb422ec5133d597
--- /dev/null
+++ b/src/test/java/caosdb/server/jobs/extension/TestAWIBoxLoan.java
@@ -0,0 +1,277 @@
+package caosdb.server.jobs.extension;
+
+import static org.junit.Assert.assertEquals;
+
+import caosdb.server.entity.container.TransactionContainer;
+import caosdb.server.jobs.core.Mode;
+import caosdb.server.transaction.Transaction;
+import caosdb.server.utils.EntityStatus;
+import java.util.Collection;
+import java.util.List;
+import java.util.concurrent.Callable;
+import org.apache.shiro.authc.AuthenticationException;
+import org.apache.shiro.authc.AuthenticationToken;
+import org.apache.shiro.authz.AuthorizationException;
+import org.apache.shiro.authz.Permission;
+import org.apache.shiro.session.Session;
+import org.apache.shiro.subject.ExecutionException;
+import org.apache.shiro.subject.PrincipalCollection;
+import org.apache.shiro.subject.Subject;
+import org.junit.Test;
+
+public class TestAWIBoxLoan {
+
+  @Test
+  public void testNonAnonymousUser() {
+    TransactionContainer container = new TransactionContainer();
+    AWIBoxLoan j =
+        new AWIBoxLoan() {
+
+          @Override
+          protected Subject getUser() {
+            return new Subject() {
+
+              @Override
+              public void runAs(PrincipalCollection principals)
+                  throws NullPointerException, IllegalStateException {}
+
+              @Override
+              public PrincipalCollection releaseRunAs() {
+                return null;
+              }
+
+              @Override
+              public void logout() {}
+
+              @Override
+              public void login(AuthenticationToken token) throws AuthenticationException {}
+
+              @Override
+              public boolean isRunAs() {
+                return false;
+              }
+
+              @Override
+              public boolean isRemembered() {
+                return false;
+              }
+
+              @Override
+              public boolean isPermittedAll(Collection<Permission> permissions) {
+                return false;
+              }
+
+              @Override
+              public boolean isPermittedAll(String... permissions) {
+                return false;
+              }
+
+              @Override
+              public boolean[] isPermitted(List<Permission> permissions) {
+                return null;
+              }
+
+              @Override
+              public boolean[] isPermitted(String... permissions) {
+                return null;
+              }
+
+              @Override
+              public boolean isPermitted(Permission permission) {
+
+                return false;
+              }
+
+              @Override
+              public boolean isPermitted(String permission) {
+
+                return false;
+              }
+
+              @Override
+              public boolean isAuthenticated() {
+
+                return false;
+              }
+
+              @Override
+              public boolean[] hasRoles(List<String> roleIdentifiers) {
+
+                return null;
+              }
+
+              @Override
+              public boolean hasRole(String roleIdentifier) {
+
+                return false;
+              }
+
+              @Override
+              public boolean hasAllRoles(Collection<String> roleIdentifiers) {
+
+                return false;
+              }
+
+              @Override
+              public Session getSession(boolean create) {
+                return null;
+              }
+
+              @Override
+              public Session getSession() {
+                return null;
+              }
+
+              @Override
+              public PrincipalCollection getPrincipals() {
+                return null;
+              }
+
+              @Override
+              public Object getPrincipal() {
+                return null;
+              }
+
+              @Override
+              public PrincipalCollection getPreviousPrincipals() {
+                return null;
+              }
+
+              @Override
+              public void execute(Runnable runnable) {}
+
+              @Override
+              public <V> V execute(Callable<V> callable) throws ExecutionException {
+                return null;
+              }
+
+              @Override
+              public void checkRoles(String... roleIdentifiers) throws AuthorizationException {}
+
+              @Override
+              public void checkRoles(Collection<String> roleIdentifiers)
+                  throws AuthorizationException {}
+
+              @Override
+              public void checkRole(String roleIdentifier) throws AuthorizationException {}
+
+              @Override
+              public void checkPermissions(Collection<Permission> permissions)
+                  throws AuthorizationException {}
+
+              @Override
+              public void checkPermissions(String... permissions) throws AuthorizationException {}
+
+              @Override
+              public void checkPermission(Permission permission) throws AuthorizationException {}
+
+              @Override
+              public void checkPermission(String permission) throws AuthorizationException {}
+
+              @Override
+              public Runnable associateWith(Runnable runnable) {
+                return null;
+              }
+
+              @Override
+              public <V> Callable<V> associateWith(Callable<V> callable) {
+                return null;
+              }
+            };
+          }
+        };
+    Transaction<TransactionContainer> t =
+        new Transaction<TransactionContainer>(container, null) {
+
+          @Override
+          protected void transaction() throws Exception {}
+
+          @Override
+          protected void preTransaction() throws InterruptedException {}
+
+          @Override
+          protected void preCheck() throws InterruptedException, Exception {}
+
+          @Override
+          protected void postTransaction() throws Exception {}
+
+          @Override
+          protected void postCheck() {}
+
+          @Override
+          public boolean logHistory() {
+            return false;
+          }
+
+          @Override
+          protected void init() throws Exception {}
+
+          @Override
+          protected void cleanUp() {}
+        };
+
+    j.init(Mode.MUST, null, t);
+    assertEquals(0, j.getContainer().getMessages().size());
+    assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus());
+
+    // non-anonymous user
+    j.run();
+    assertEquals(0, j.getContainer().getMessages().size());
+    assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus());
+  }
+
+  @Test
+  public void testAnonymousUserUnqualified() {
+    TransactionContainer container = new TransactionContainer();
+    AWIBoxLoan j =
+        new AWIBoxLoan() {
+
+          @Override
+          protected Subject getUser() {
+            return null;
+          }
+
+          @Override
+          boolean isAnonymous() {
+            return true;
+          }
+        };
+    Transaction<TransactionContainer> t =
+        new Transaction<TransactionContainer>(container, null) {
+
+          @Override
+          protected void transaction() throws Exception {}
+
+          @Override
+          protected void preTransaction() throws InterruptedException {}
+
+          @Override
+          protected void preCheck() throws InterruptedException, Exception {}
+
+          @Override
+          protected void postTransaction() throws Exception {}
+
+          @Override
+          protected void postCheck() {}
+
+          @Override
+          public boolean logHistory() {
+            return false;
+          }
+
+          @Override
+          protected void init() throws Exception {}
+
+          @Override
+          protected void cleanUp() {}
+        };
+
+    j.init(Mode.MUST, null, t);
+    assertEquals(0, j.getContainer().getMessages().size());
+    assertEquals(EntityStatus.QUALIFIED, j.getContainer().getStatus());
+
+    j.run();
+    assertEquals(1, j.getContainer().getMessages().size());
+    assertEquals(EntityStatus.UNQUALIFIED, j.getContainer().getStatus());
+  }
+}