diff --git a/src/doc/index.rst b/src/doc/index.rst
index 4f63fe07f37a4432a442040ff2f614f01c959472..b5ce9f3235277b613b357dca5dfc3334d80aeb3f 100644
--- a/src/doc/index.rst
+++ b/src/doc/index.rst
@@ -10,6 +10,7 @@ Welcome to caosdb-server's documentation!
 
    Getting started <README_SETUP>
    Concepts <concepts>
+   tutorials
    Query Language <CaosDB-Query-Language>
    administration
    Development <development/devel>
diff --git a/src/doc/tutorials.rst b/src/doc/tutorials.rst
new file mode 100644
index 0000000000000000000000000000000000000000..27aacfb3eb9a4f04ab261b12f904e652c4daf3d3
--- /dev/null
+++ b/src/doc/tutorials.rst
@@ -0,0 +1,10 @@
+Tutorials
+==============
+
+.. toctree::
+   :maxdepth: 1
+   :glob:
+
+   tutorials/*
+
+
diff --git a/src/doc/tutorials/setup_state_model.py b/src/doc/tutorials/setup_state_model.py
new file mode 100755
index 0000000000000000000000000000000000000000..0a1a7daa3b14d7c3e7a5b8eac093857bddfad330
--- /dev/null
+++ b/src/doc/tutorials/setup_state_model.py
@@ -0,0 +1,379 @@
+#!/usr/bin/env python3
+# encoding: utf-8
+#
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2021 Indiscale GmbH <info@indiscale.com>
+# Copyright (C) 2021 Henrik tom Wörden <h.tomwoerden@indiscale.com>
+# Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+"""
+This is a utility script to setup a publication process in LinkAhead using
+states.
+
+If you start from scratch you should perform the following actions in that
+order:
+
+1. setup_roles
+2. setup_state_data_model
+4. setup_model_publication_cycle
+"""
+from argparse import ArgumentParser, RawDescriptionHelpFormatter
+
+import caosdb as db
+from caosdb.common.administration import generate_password
+
+
+def teardown(args):
+    """fully clears the database"""
+
+    if "yes" != input(
+        "Are you really sure that you want to delete ALL "
+        "ENTITIES in LinkAhead? [yes/No]"
+    ):
+
+        print("Nothing done.")
+
+        return
+    d = db.execute_query("FIND ENTITY WITH ID > 99")
+
+    if len(d) > 0:
+        d.delete(flags={"forceFinalState": "true"})
+
+
+def soft_teardown(args):
+    """ allows to remove state data only """
+    recs = db.execute_query("FIND Entity WITH State")
+
+    for rec in recs:
+        rec.state = None
+    recs.update(flags={"forceFinalState": "true"})
+    db.execute_query("FIND StateModel").delete()
+    db.execute_query("FIND Transition").delete()
+    db.execute_query("FIND State").delete()
+    db.execute_query(
+        "FIND Property WITH name=from or name=to or name=initial or name=final or name=color").delete()
+
+
+def setup_user(args):
+    """Creates a user with given username and adds the given role.
+
+    If the user exists, it is deleted first. A random password is generated
+    and printed in clear text in the console output.
+
+    """
+
+    username, role = args.username, args.role
+    try:
+        db.administration._delete_user(name=username)
+    except Exception:
+        pass
+
+    password = generate_password(10)
+    print("new password for {}:\n{}".format(username, password))
+    db.administration._insert_user(
+        name=username, password=password, status="ACTIVE")
+    db.administration._set_roles(username=username, roles=[role])
+
+
+def remove_user(args):
+    """deletes the given user"""
+    db.administration._delete_user(name=args.username)
+
+
+def setup_role_permissions():
+    """
+    Adds the appropriate permissions to the 'normal' and 'publisher' role.
+
+    The permissions are such that they suit the publication life cycle.
+    """
+    db.administration._set_permissions(
+        role="normal",
+        permission_rules=[
+            db.administration.PermissionRule("Grant", "TRANSACTION:*"),
+            db.administration.PermissionRule(
+                "Grant", "ACM:USER:UPDATE_PASSWORD:?REALM?:?USERNAME?"
+            ),
+            db.administration.PermissionRule("Grant", "STATE:TRANSITION:Edit"),
+            db.administration.PermissionRule("Grant", "UPDATE:PROPERTY:ADD"),
+            db.administration.PermissionRule(
+                "Grant", "UPDATE:PROPERTY:REMOVE"),
+            db.administration.PermissionRule(
+                "Grant", "STATE:TRANSITION:Start Review"),
+            db.administration.PermissionRule(
+                "Grant", "STATE:ASSIGN:Publish Life-cycle"
+            ),
+        ],
+    )
+
+    db.administration._set_permissions(
+        role="publisher",
+        permission_rules=[
+            db.administration.PermissionRule(
+                "Grant", "ACM:USER:UPDATE_PASSWORD:?REALM?:?USERNAME?"
+            ),
+            db.administration.PermissionRule("Grant", "TRANSACTION:*"),
+            db.administration.PermissionRule("Grant", "UPDATE:PROPERTY:ADD"),
+            db.administration.PermissionRule(
+                "Grant", "UPDATE:PROPERTY:REMOVE"),
+            db.administration.PermissionRule("Grant", "STATE:*"),
+        ],
+    )
+
+
+def setup_roles(args):
+    """Creates 'publisher' and 'normla' roles and assigns appropriate
+    permissions
+
+    If those roles exist they are deleted first.
+    """
+
+    for role in ["publisher", "normal"]:
+        try:
+            db.administration._delete_role(name=role)
+        except Exception:
+            print("Could not delete role {}".format(role))
+
+    for role in ["publisher", "normal"]:
+        db.administration._insert_role(name=role, description="")
+
+    setup_role_permissions()
+
+
+def setup_state_data_model(args):
+    """Creates the data model for using states
+
+    RecordTypes: State, StateModel, Transition
+    Properties: from, to, initial, final, color
+    """
+    cont = db.Container().extend(
+        [
+            db.RecordType("State"),
+            db.RecordType("StateModel"),
+            db.RecordType("Transition"),
+            db.Property(name="from", datatype="State"),
+            db.Property(name="to", datatype="State"),
+            db.Property(name="initial", datatype="State"),
+            db.Property(name="final", datatype="State"),
+            db.Property(name="color", datatype=db.TEXT),
+        ]
+    )
+    cont.insert()
+
+
+def setup_model_publication_cycle(args):
+    """Creates States and Transitions for the Publication Life Cycle"""
+    unpublished_acl = db.ACL()
+    unpublished_acl.grant(role="publisher", permission="*")
+    unpublished_acl.grant(role="normal", permission="UPDATE:*")
+    unpublished_acl.grant(role="normal", permission="RETRIEVE:ENTITY")
+    unpublished_acl = db.State.create_state_acl(unpublished_acl)
+
+    unpublished_state = (
+        db.Record(
+            "Unpublished",
+            description="Unpublished entries are only visible to the team "
+            "and may be edited by any team member.",
+        )
+        .add_parent("State")
+        .add_property("color", "#5bc0de")
+    )
+    unpublished_state.acl = unpublished_acl
+    unpublished_state.insert()
+
+    review_acl = db.ACL()
+    review_acl.grant(role="publisher", permission="*")
+    review_acl.grant(role="normal", permission="RETRIEVE:ENTITY")
+
+    review_state = (
+        db.Record(
+            "Under Review",
+            description="Entries under review are not publicly available yet, "
+            "but they can only be edited by the members of the publisher "
+            "group.",
+        )
+        .add_parent("State")
+        .add_property("color", "#FFCC33")
+    )
+    review_state.acl = db.State.create_state_acl(review_acl)
+    review_state.insert()
+
+    published_acl = db.ACL()
+    published_acl.grant(role="guest", permission="RETRIEVE:ENTITY")
+
+    published_state = (
+        db.Record(
+            "Published",
+            description="Published entries are publicly available and "
+            "cannot be edited unless they are unpublished again.",
+        )
+        .add_parent("State")
+        .add_property("color", "#333333")
+    )
+    published_state.acl = db.State.create_state_acl(published_acl)
+    published_state.insert()
+
+    # 1->2
+    (
+        db.Record(
+            "Start Review",
+            description="This transitions denies the permissions to edit an "
+            "entry for anyone but the members of the publisher group. "
+            "However, the entry is not yet publicly available.",
+        )
+        .add_parent("Transition")
+        .add_property("from", "unpublished")
+        .add_property("to", "under review")
+        .add_property("color", "#FFCC33")
+        .insert()
+    )
+
+    # 2->3
+    (
+        db.Record(
+            "Publish",
+            description="Published entries are visible for the public and "
+            "cannot be changed unless they are unpublished again. Only members"
+            " of the publisher group can publish or unpublish entries.",
+        )
+        .add_parent("Transition")
+        .add_property("from", "under review")
+        .add_property("to", "published")
+        .add_property("color", "red")
+        .insert()
+    )
+
+    # 3->1
+    (
+        db.Record(
+            "Unpublish",
+            description="Unpublish this entry to hide it from "
+            "the public. Unpublished entries can be edited by any team "
+            "member.",
+        )
+        .add_parent("Transition")
+        .add_property("from", "published")
+        .add_property("to", "unpublished")
+        .insert()
+    )
+
+    # 2->1
+    (
+        db.Record(
+            "Reject",
+            description="Reject the publishing of this entity.  Afterwards, "
+            "the entity is editable for any team member again.",
+        )
+        .add_parent("Transition")
+        .add_property("from", "under review")
+        .add_property("to", "unpublished")
+        .insert()
+    )
+
+    # 1->1
+    (
+        db.Record(
+            "Edit",
+            description="Edit this entity. The changes are not publicly "
+            "available until this entity will have been reviewed and "
+            "published.",
+        )
+        .add_parent(
+            "Transition",
+        )
+        .add_property("from", "unpublished")
+        .add_property("to", "unpublished")
+        .insert()
+    )
+
+    (
+        db.Record(
+            "Publish Life-cycle",
+            description="The publish life-cycle is a quality assurance tool. "
+            "Database entries can be edited without being publicly available "
+            "until the changes have been reviewed and explicitely published by"
+            " an eligible user.",
+        )
+        .add_parent("StateModel")
+        .add_property(
+            "Transition",
+            datatype=db.LIST("Transition"),
+            value=[
+                "Edit",
+                "Start Review",
+                "Reject",
+                "Publish",
+                "Unpublish",
+            ],
+        )
+        .add_property("initial", "Unpublished")
+        .add_property("final", "Unpublished")
+        .insert()
+    )
+
+
+def parse_args():
+    parser = ArgumentParser(
+        description=__doc__, formatter_class=RawDescriptionHelpFormatter
+    )
+    subparsers = parser.add_subparsers(
+        title="action",
+        metavar="ACTION",
+        description=(
+            "You can perform the following actions. "
+            "Print the detailed help for each command with "
+            "#> setup_state_model ACTION -h"
+        ),
+    )
+
+    subparser = subparsers.add_parser(
+        "setup_state_data_model", help=setup_state_data_model.__doc__
+    )
+    subparser.set_defaults(call=setup_state_data_model)
+
+    subparser = subparsers.add_parser(
+        "setup_model_publication_cycle", help=setup_model_publication_cycle.__doc__
+    )
+    subparser.set_defaults(call=setup_model_publication_cycle)
+
+    subparser = subparsers.add_parser("setup_roles", help=setup_roles.__doc__)
+    subparser.set_defaults(call=setup_roles)
+
+    subparser = subparsers.add_parser("remove_user", help=remove_user.__doc__)
+    subparser.set_defaults(call=remove_user)
+    subparser.add_argument("username")
+
+    subparser = subparsers.add_parser("setup_user", help=setup_user.__doc__)
+    subparser.set_defaults(call=setup_user)
+    subparser.add_argument("username")
+    subparser.add_argument("role")
+
+    subparser = subparsers.add_parser(
+        "teardown", help="Removes ALL ENTITIES from LinkAhead!"
+    )
+    subparser.set_defaults(call=teardown)
+
+    subparser = subparsers.add_parser(
+        "soft_teardown", help=soft_teardown.__doc__
+    )
+    subparser.set_defaults(call=soft_teardown)
+
+    return parser.parse_args()
+
+
+if __name__ == "__main__":
+    args = parse_args()
+    args.call(args)
diff --git a/src/doc/tutorials/statemachine.rst b/src/doc/tutorials/statemachine.rst
new file mode 100644
index 0000000000000000000000000000000000000000..317620423e6221ccaf29297fc1f71f0c008f78a0
--- /dev/null
+++ b/src/doc/tutorials/statemachine.rst
@@ -0,0 +1,34 @@
+
+State Machine
+=============
+
+Prerequisites
+-------------
+
+In order to use the state machine functionality you have to set the
+corresponding server setting: ``EXT_ENTITY_STATE=ENABLED``.
+
+Also, a few RecordTypes and Properties are required. You can use the
+script `setup_state_model.py <https://gitlab.com/caosdb/caosdb-server/-/blob/dev/src/doc/tutorials/setup_state_model.py>`_
+to create those or you may have a look at it to see what is needed (``setup_state_data_model`` function).
+
+Defining the State Machine
+--------------------------
+
+Now you are setup to create your own state machine. You can define States and Transitions
+and bundle it all to a StateModel. The above mentioned ``setup_state_model.py`` script defines
+a publication cycle with the state "Unpublished", "UnderReview" and "Published".
+Again, the ``setup_state_model.py`` script provides orientation on how this
+can be setup (``setup_model_publication_cycle`` function).
+
+Note, that you can provide ACL to the state definition which will be applied to an entity once
+the state is reached. This is for example useful to change the visibility depending on a state change.
+
+If you assign a state to a RecordType, this state will be the initial state
+of Records that have that parent. For example by executing:
+
+.. code-block:: Python
+
+    rt = db.RecordType("Article").retrieve()
+    rt.state = db.State(name="UnPublished", model="Publish Life-cycle")``
+    rt.update()