diff --git a/aleksis/apps/alsijil/forms.py b/aleksis/apps/alsijil/forms.py
index 6f4e65b2022192c3cf5c0a7c4b44cc3e1731919e..e325586cbf8f0a1bace8c186fc85e76b309e0d35 100644
--- a/aleksis/apps/alsijil/forms.py
+++ b/aleksis/apps/alsijil/forms.py
@@ -31,6 +31,7 @@ from .models import (
     ExtraMark,
     GroupRole,
     GroupRoleAssignment,
+    Instruction,
     LessonDocumentation,
     PersonalNote,
 )
@@ -383,3 +384,11 @@ class RegisterObjectActionForm(ListActionForm):
     """Action form for managing register objects for use with ``RegisterObjectTable``."""
 
     actions = [send_request_to_check_entry]
+
+
+class InstructionForm(forms.ModelForm):
+    layout = Layout("name", "icon", "pdf_file", "groups")
+
+    class Meta:
+        model = Instruction
+        fields = ["name", "icon", "pdf_file", "groups"]
diff --git a/aleksis/apps/alsijil/migrations/0014_instruction.py b/aleksis/apps/alsijil/migrations/0017_instruction.py
similarity index 99%
rename from aleksis/apps/alsijil/migrations/0014_instruction.py
rename to aleksis/apps/alsijil/migrations/0017_instruction.py
index 48525627f2fdbc0950fc1bddfbf2bb10e09eebf1..11e200873c02ce578fda5364804bad0efc363dba 100644
--- a/aleksis/apps/alsijil/migrations/0014_instruction.py
+++ b/aleksis/apps/alsijil/migrations/0017_instruction.py
@@ -10,7 +10,7 @@ class Migration(migrations.Migration):
     dependencies = [
         ('core', '0019_fix_uniqueness_per_site'),
         ('sites', '0002_alter_domain_unique'),
-        ('alsijil', '0013_fix_uniqueness_per_site'),
+        ('alsijil', '0016_add_not_counted_excuse_types'),
     ]
 
     operations = [
diff --git a/aleksis/apps/alsijil/models.py b/aleksis/apps/alsijil/models.py
index 42189856b660e771f0dd36efd14414450a6485c9..90c2868ba3f1a625d83f045dc9dc0d15a0c31898 100644
--- a/aleksis/apps/alsijil/models.py
+++ b/aleksis/apps/alsijil/models.py
@@ -525,6 +525,7 @@ class Instruction(SchoolTermRelatedExtensibleModel):
         help_text=_(
             "The instruction will be shown for the members and owners of the selected groups."
         ),
+        related_name="instructions",
     )
 
     def __str__(self):
diff --git a/aleksis/apps/alsijil/rules.py b/aleksis/apps/alsijil/rules.py
index 621f3e3b2e6994fc2f06b0081d6fa8c191eca641..aa80713bda0ce2e643e03e1a0f2f484b7aea297b 100644
--- a/aleksis/apps/alsijil/rules.py
+++ b/aleksis/apps/alsijil/rules.py
@@ -11,12 +11,14 @@ from aleksis.core.util.predicates import (
 )
 
 from .util.predicates import (
+    has_any_instruction,
     has_lesson_group_object_perm,
     has_person_group_object_perm,
     has_personal_note_group_perm,
     is_group_member,
     is_group_owner,
     is_group_role_assignment_group_owner,
+    is_instruction_for_person,
     is_lesson_original_teacher,
     is_lesson_parent_group_owner,
     is_lesson_participant,
@@ -311,5 +313,27 @@ view_register_objects_list_predicate = has_person & (
 )
 add_perm("alsijil.view_register_objects_list_rule", view_register_objects_list_predicate)
 
-view_instructions_predicate = has_person
+view_instructions_predicate = has_person & (
+    has_global_perm("alsijil.view_instruction") | has_any_instruction
+)
 add_perm("alsijil.view_instructions_rule", view_instructions_predicate)
+
+view_instruction_predicate = has_person & (
+    has_global_perm("alsijil.view_instruction")
+    | is_instruction_for_person
+    | has_object_perm("alsijil.view_instruction")
+)
+add_perm("alsijil.view_instruction_rule", view_instruction_predicate)
+
+add_instruction_predicate = view_instructions_predicate & has_global_perm("alsijil.add_instruction")
+add_perm("alsijil.add_instruction_rule", add_instruction_predicate)
+
+edit_instruction_predicate = view_instructions_predicate & (
+    has_global_perm("alsijil.change_instruction") | has_object_perm("alsijil.change_instruction")
+)
+add_perm("alsijil.edit_instruction_rule", edit_instruction_predicate)
+
+delete_instruction_predicate = view_instructions_predicate & (
+    has_global_perm("alsijil.delete_instruction") | has_object_perm("alsijil.delete_instruction")
+)
+add_perm("alsijil.delete_instruction_rule", delete_instruction_predicate)
diff --git a/aleksis/apps/alsijil/templates/alsijil/instruction/create.html b/aleksis/apps/alsijil/templates/alsijil/instruction/create.html
new file mode 100644
index 0000000000000000000000000000000000000000..42761a81361d4e86b085082cfa5500360426ac3a
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/instruction/create.html
@@ -0,0 +1,15 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Create instruction{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Create instruction{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <form method="post" enctype="multipart/form-data">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+{% endblock %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/instruction/edit.html b/aleksis/apps/alsijil/templates/alsijil/instruction/edit.html
new file mode 100644
index 0000000000000000000000000000000000000000..b9732e4c41c93c7e17f498fb942581921bc3b7f0
--- /dev/null
+++ b/aleksis/apps/alsijil/templates/alsijil/instruction/edit.html
@@ -0,0 +1,17 @@
+{# -*- engine:django -*- #}
+
+{% extends "core/base.html" %}
+{% load material_form i18n %}
+
+{% block browser_title %}{% blocktrans %}Edit instruction{% endblocktrans %}{% endblock %}
+{% block page_title %}{% blocktrans %}Edit instruction{% endblocktrans %}{% endblock %}
+
+{% block content %}
+  <form method="post" enctype="multipart/form-data">
+    {% csrf_token %}
+    {% form form=form %}{% endform %}
+    {% include "core/partials/save_button.html" %}
+  </form>
+
+  {{ form.media.js }}
+{% endblock %}
diff --git a/aleksis/apps/alsijil/templates/alsijil/instruction/list.html b/aleksis/apps/alsijil/templates/alsijil/instruction/list.html
index da3c384b39440f78e27ac4700124a3ed58d3984b..02320901a0bfc927d3c2e95dd174e26f24904370 100644
--- a/aleksis/apps/alsijil/templates/alsijil/instruction/list.html
+++ b/aleksis/apps/alsijil/templates/alsijil/instruction/list.html
@@ -2,21 +2,51 @@
 
 {% extends "core/base.html" %}
 
-{% load i18n %}
+{% load i18n rules %}
 
 {% block browser_title %}{% blocktrans %}Instructions{% endblocktrans %}{% endblock %}
 {% block page_title %}{% blocktrans %}Instructions{% endblocktrans %}{% endblock %}
 
 {% block content %}
+  {% has_perm "alsijil.add_instruction_rule" user as can_add %}
+  {% if can_add %}
+    <a class="btn green waves-effect waves-light" href="{% url 'create_instruction' %}">
+      <i class="material-icons left">add</i>
+      {% trans "Create instruction" %}
+    </a>
+  {% endif %}
+
   <ul class="collection">
     {% for instruction in instruction_list %}
       <li class="collection-item avatar">
         <i class="material-icons materialize-circle primary-color">{{ instruction.icon|default:"rule" }}</i>
         <span class="title"> {{ instruction.name }}</span>
+        <div class="right">
+          {% has_perm "alsijil.edit_instruction_rule" user as can_edit %}
+          {% has_perm "alsijil.delete_instruction_rule" user as can_delete %}
+          {% if can_edit %}
+            <a class="btn-flat waves-effect waves-orange orange-text"
+               href="{% url "edit_instruction" instruction.pk %}">
+              <i class="material-icons left">edit</i>
+              {% trans "Edit" %}
+            </a>
+          {% endif %}
+          {% if can_delete %}
+            <a class="btn-flat waves-effect waves-red red-text" href="{% url "delete_instruction" instruction.pk %}">
+              <i class="material-icons left">delete</i>
+              {% trans "Delete" %}
+            </a>
+          {% endif %}
+
           <a class="btn-flat waves-effect waves-green right" href="{{ instruction.pdf_file.url }}" target="_blank">
             <i class="material-icons left">picture_as_pdf</i>
             {% trans "Show PDF file with instruction" %}
           </a>
+        </div>
+      </li>
+    {% empty %}
+      <li class="collection-item">
+        {% trans "No instructions available." %}
       </li>
     {% endfor %}
   </ul>
diff --git a/aleksis/apps/alsijil/urls.py b/aleksis/apps/alsijil/urls.py
index f7551eb8eb53e4bd21b639eb8b67d0a69daf0ced..19f92a2354bbd4810a38cd6e40b5347c240cfd38 100644
--- a/aleksis/apps/alsijil/urls.py
+++ b/aleksis/apps/alsijil/urls.py
@@ -129,4 +129,13 @@ urlpatterns = [
     ),
     path("all/", views.AllRegisterObjectsView.as_view(), name="all_register_objects"),
     path("instructions/", views.InstructionsListView.as_view(), name="instructions"),
+    path("instructions/create/", views.InstructionCreateView.as_view(), name="create_instruction"),
+    path(
+        "instructions/<int:pk>/edit/", views.InstructionEditView.as_view(), name="edit_instruction"
+    ),
+    path(
+        "instructions/<int:pk>/delete/",
+        views.InstructionDeleteView.as_view(),
+        name="delete_instruction",
+    ),
 ]
diff --git a/aleksis/apps/alsijil/util/predicates.py b/aleksis/apps/alsijil/util/predicates.py
index 1759a5446d204d61c87f5bd1cbe9e3e031d95be2..377779d30e9c01de09f48f8befd9921326dcb010 100644
--- a/aleksis/apps/alsijil/util/predicates.py
+++ b/aleksis/apps/alsijil/util/predicates.py
@@ -1,6 +1,7 @@
 from typing import Any, Union
 
 from django.contrib.auth.models import User
+from django.db.models import Q
 
 from rules import predicate
 
@@ -8,7 +9,7 @@ from aleksis.apps.chronos.models import Event, ExtraLesson, LessonPeriod
 from aleksis.core.models import Group, Person
 from aleksis.core.util.predicates import check_object_permission
 
-from ..models import PersonalNote
+from ..models import Instruction, PersonalNote
 
 
 @predicate
@@ -281,3 +282,22 @@ def is_group_role_assignment_group_owner(user: User, obj: Union[Group, Person])
 def is_owner_of_any_group(user: User, obj):
     """Predicate which checks if the person is group owner of any group."""
     return Group.objects.filter(owners=user.person).exists()
+
+
+@predicate
+def has_any_instruction(user: User, obj):
+    """Predicate which checks if the user has any instruction."""
+    return Instruction.objects.filter(
+        Q(groups__members=user.person) | Q(groups__owners=user.person)
+    ).exists()
+
+
+@predicate
+def is_instruction_for_person(user: User, obj: Instruction):
+    """Predicate which checks if the instruction is for the person."""
+    return (
+        user
+        in Person.objects.filter(
+            Q(member_of__instructions=obj) | Q(owner_of__instructions=obj)
+        ).exists()
+    )
diff --git a/aleksis/apps/alsijil/views.py b/aleksis/apps/alsijil/views.py
index 17b6e2dee3b7e0a1d28fea2986ba6a4b8d11571e..f05b90fb4e569920d18465f4537582c1c2f0d138 100644
--- a/aleksis/apps/alsijil/views.py
+++ b/aleksis/apps/alsijil/views.py
@@ -51,6 +51,7 @@ from .forms import (
     FilterRegisterObjectForm,
     GroupRoleAssignmentEditForm,
     GroupRoleForm,
+    InstructionForm,
     LessonDocumentationForm,
     PersonalNoteFormSet,
     PersonOverviewForm,
@@ -1405,8 +1406,56 @@ class AllRegisterObjectsView(PermissionRequiredMixin, View):
 
 
 class InstructionsListView(PermissionRequiredMixin, ListView):
-    """Table of all excuse types."""
+    """Table of all instructions."""
 
     model = Instruction
     permission_required = "alsijil.view_instructions_rule"
     template_name = "alsijil/instruction/list.html"
+
+    def get_queryset(self):
+        if self.request.user.has_perm("alsijil.view_instruction"):
+            return super().get_queryset()
+        return (
+            super()
+            .get_queryset()
+            .filter(
+                Q(groups__members=self.request.user.person)
+                | Q(groups__owners=self.request.user.person)
+            )
+            .distinct()
+        )
+
+
+@method_decorator(never_cache, name="dispatch")
+class InstructionCreateView(PermissionRequiredMixin, AdvancedCreateView):
+    """Create view for instructions."""
+
+    model = Instruction
+    form_class = InstructionForm
+    permission_required = "alsijil.add_instruction_rule"
+    template_name = "alsijil/instruction/create.html"
+    success_url = reverse_lazy("instructions")
+    success_message = _("The instruction has been created.")
+
+
+@method_decorator(never_cache, name="dispatch")
+class InstructionEditView(PermissionRequiredMixin, AdvancedEditView):
+    """Edit view for instructions."""
+
+    model = Instruction
+    form_class = InstructionForm
+    permission_required = "alsijil.edit_instruction_rule"
+    template_name = "alsijil/instruction/edit.html"
+    success_url = reverse_lazy("instructions")
+    success_message = _("The instruction has been saved.")
+
+
+@method_decorator(never_cache, "dispatch")
+class InstructionDeleteView(PermissionRequiredMixin, RevisionMixin, AdvancedDeleteView):
+    """Delete view for instructions."""
+
+    model = Instruction
+    permission_required = "alsijil.delete_instruction_rule"
+    template_name = "core/pages/delete.html"
+    success_url = reverse_lazy("instructions")
+    success_message = _("The instruction has been deleted.")