diff --git a/hiboo/application/__init__.py b/hiboo/application/__init__.py
index b2de80b5a1bbeaf8e41b0212266a6725050ea4b1..062c565b1f7bed2d41aa6d98b6972aef123ec2ee 100644
--- a/hiboo/application/__init__.py
+++ b/hiboo/application/__init__.py
@@ -1,3 +1,7 @@
+# TODO: remove profile_regex
+# This allows to avoid passing the service object anywhere, and manipulate
+# the service config dictionary instead
+
 import flask
 
 
diff --git a/hiboo/application/base.py b/hiboo/application/base.py
index eb5b64d417ce978d64b97752239219dbfe7036a4..4aeeed1bd8f406f68e338dea4afd08fc5d511b6f 100644
--- a/hiboo/application/base.py
+++ b/hiboo/application/base.py
@@ -31,12 +31,43 @@ class BaseForm(flask_wtf.FlaskForm):
     submit = fields.SubmitField(_('Submit'))
 
 
+def action(label, profile=False, quick=False):
+    """ Registers a profile or application action
+    """
+    class Register():
+        def __init__(self, function):
+            self.function = function        
+        def __set_name__(self, owner, name):
+            if "actions" not in owner.__dict__:
+                owner.actions = dict()
+            owner.actions[name] = (label, profile, quick)
+            setattr(owner, name, self.function)
+    return Register
+
+
+def hook(step="pre", *transitions):
+    """ Registers a hook for given transitions
+    """
+    class Register():
+        def __init__(self, function):
+            self.function = function        
+        def __set_name__(self, owner, name):
+            if not "hooks" in owner.__dict__:
+                owner.hooks = owner.hooks[:]
+            owner.hooks.append((
+                step, transitions, self.function
+            ))
+            setattr(owner, name, self.function)
+    return Register
+
+
 class BaseApplication(object):
     """ Base application class, that provides basic behavior and registry
     """
 
     registry = dict()
     actions = dict()
+    hooks = list()
     sso_protocol = None
     name = None
 
@@ -62,31 +93,24 @@ class BaseApplication(object):
         """ Updates the service options dictionary based on SSO
         parameters
         """
+        # TODO: replace this by returning a dict and updating
+        # service config, no need to pass the service object
         return self.sso_protocol.fill_service(service)
 
-    def activate(self, profile):
-        """ Default activation behavior is doing nothing
+    def apply_hooks(self, transition, step, profile):
+        """ Apply hooks for a given transition and step
         """
-        return True
-
-    def block(self, profile):
-        """ Default blocking behavior is doing nothing
-        """
-        return True
-
-    def welcome(self, profile):
-        """ Defaul welcome behavior is doing nothing
-        """
-        return True
-
-    def notify(self, profile):
-        """ Default notifying behavior is doing nothing
-        """
-        return True
-
-    def delete(self, profile):
-        """ Deleting an account generally requires application
-        implementation, so this needs to be overridden
+        results = [
+            function(self, profile)
+            for transition_step, transitions, function in self.hooks
+            if step == transition_step and transition in transitions
+        ]
+        print(results)
+        return False in results and not True in results
+
+    @hook("pre", "purge", "purge-blocked")
+    def manual_purge(self, profile):
+        """ The default behavior is to wait for manual purge
         """
         return False
 
@@ -97,17 +121,3 @@ class OIDCApplication(BaseApplication):
 
 class SAMLApplication(BaseApplication):
     sso_protocol = sso.saml
-
-
-def action(label, profile=False, quick=False):
-    """ Registers a profile or application action
-    """
-    class Register():
-        def __init__(self, function):
-            self.function = function        
-        def __set_name__(self, owner, name):
-            if not owner.actions:
-                owner.actions = dict()
-            owner.actions[name] = (label, profile, quick)
-            setattr(owner, name, self.function)
-    return Register
\ No newline at end of file
diff --git a/hiboo/application/social.py b/hiboo/application/social.py
index cdbd9999acf7107afa8721bf564b938a20d6985a..638b872a15457d6b12a5c873750c9966844b59c0 100644
--- a/hiboo/application/social.py
+++ b/hiboo/application/social.py
@@ -59,6 +59,25 @@ class SynapseApplication(base.SAMLApplication):
             application_uri=service.config.get("application_uri")
         )
 
+    @base.hook("pre", "block", "delete")
+    def test_hooks(self, profile):
+        """ TODO: testing hooks
+        """
+        print("Hoooooked!")
+
+    @base.hook("post", "block", "purge")
+    def test_hooks2(self, profile):
+        """ TODO: testing hooks
+        """
+        print("Hooked after")
+
+    @base.hook("pre", "purge")
+    def test_hooks3(self, profile):
+        """ TODO: testing hooks
+        """
+        print("Purging the account")
+        return True
+
 
 @register("writefreely")
 class WriteFreelyApplication(base.OIDCApplication):
diff --git a/hiboo/models.py b/hiboo/models.py
index 20f5d67db663322cf754471026c162c8a5bbf9d9..999ca7be785fb4d60cc76a9f34df2b7dafe024e3 100644
--- a/hiboo/models.py
+++ b/hiboo/models.py
@@ -176,13 +176,28 @@ class Profile(db.Model):
     ACTIVE = "active"
     BLOCKED = "blocked"
     DELETED = "deleted"
+    PURGED = "purged"
 
     STATUSES = {
         UNCLAIMED: ("gray", _("unclaimed")),
         REQUEST: ("blue", _("requested")),
         ACTIVE: ("green", _("active")),
         BLOCKED: ("orange", _("blocked")),
-        DELETED: ("red", _("deleted"))
+        DELETED: ("red", _("deleted")),
+        PURGED: ("red", _("purged"))
+    }
+
+    TRANSITIONS = {
+        # Assigning or claiming is not a generic transition
+        # so it is not listed here (it requires chosing a user)
+        "activate": (REQUEST, ACTIVE, 300, _("activate")),
+        "reject": (REQUEST, DELETED, 3600 * 24 * 3, _("reject")),
+        "block": (ACTIVE, BLOCKED, 0, _("block")),
+        "unblock": (BLOCKED, ACTIVE, 300, _("unblock")),
+        "delete": (ACTIVE, DELETED, 300, _("delete")),
+        "delete-blocked": (BLOCKED, DELETED, 300, _("delete")),
+        "purge": (ACTIVE, PURGED, 3600 * 24 * 3, _("purge")),
+        "purge-blocked": (BLOCKED, PURGED, 300, _("purge"))
     }
 
     user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid))
@@ -201,6 +216,13 @@ class Profile(db.Model):
     def email(self):
         return "{}@{}".format(self.uuid, app.config.get("MAIL_DOMAIN"))
 
+    @property
+    def transitions(self):
+        return {
+            name: transition for name, transition in Profile.TRANSITIONS.items()
+            if transition[0] == self.status and not self.transition
+        }
+
     @classmethod
     def filter(cls, service, user):
         return cls.query.filter_by(
@@ -242,13 +264,15 @@ class History(db.Model):
     DELETE = "delete"
     NOTE = "note"
     STATUS = "status"
+    TRANSITION = "transition"
     PASSWORD = "password"
 
     DESCRIPTION = {
         SIGNUP: _("signed up for this account"),
         CREATE: _("created the profile {this.profile.username} on {this.service.name}"),
         PASSWORD: _("changed this account password"),
-        STATUS: _("set the {this.service.name} profile {this.profile.username} as {this.value}")
+        STATUS: _("set the {this.service.name} profile {this.profile.username} as {this.value}"),
+        TRANSITION: _("did {this.transition[3]} the profile {this.profile.username} on {this.service.name}")
     }
 
     user_uuid = db.Column(db.String(36), db.ForeignKey(User.uuid))
@@ -268,6 +292,10 @@ class History(db.Model):
     category = db.Column(db.String(25))
     value = db.Column(db.String())
 
+    @property
+    def transition(self):
+        return Profile.TRANSITIONS[self.value]
+
     @property
     def description(self):
         return History.DESCRIPTION.get(self.category, "").format(this=self)
diff --git a/hiboo/profile/common.py b/hiboo/profile/common.py
new file mode 100644
index 0000000000000000000000000000000000000000..ad187a4f96aa2105efc260fd7cf8639fa90a0005
--- /dev/null
+++ b/hiboo/profile/common.py
@@ -0,0 +1,11 @@
+from hiboo import models
+
+
+def apply_transition(profile, current, target, delay, transition, step="pre"):
+    """ Handle profile transitions as single-step workflows
+    """
+    app = profile.service.application
+    manual = app.apply_hooks(transition, step, profile)
+    if step == "pre" and not manual:
+        profile.status, profile.transition = target, None
+        apply_transition(profile, current, target, delay, transition, "post")
diff --git a/hiboo/profile/templates/profile_details.html b/hiboo/profile/templates/profile_details.html
index 1c9c8839d7be03ee53994ba9d92fc132a0378af0..50e36864dbb1e347aaff11783b80e321e3e4aa29 100644
--- a/hiboo/profile/templates/profile_details.html
+++ b/hiboo/profile/templates/profile_details.html
@@ -36,13 +36,7 @@
 {% endblock %}
 
 {% block actions %}
-{% if profile.status == "active" %}
-<a href="{{ url_for("profile.set_status", profile_uuid=profile.uuid, status="blocked") }}" class="btn btn-warning">{% trans %}Block profile{% endtrans %}</a>
-{% elif profile.status == "blocked" %}
-<a href="{{ url_for("profile.set_status", profile_uuid=profile.uuid, status="active") }}" class="btn btn-success">{% trans %}Unblock profile{% endtrans %}</a>
-{% elif profile.status == "request" %}
-<a href="{{ url_for("profile.set_status", profile_uuid=profile.uuid, status="active") }}" class="btn btn-success">{% trans %}Validate profile{% endtrans %}</a>
-{% elif profile.status == "unclaimed" %}
-<a href="{{ url_for("profile.assign", profile_uuid=profile.uuid) }}" class="btn btn-info">{% trans %}Validate profile{% endtrans %}</a>
-{% endif %}
+{% for transition, (_, _, _, label) in profile.transitions.items() %}
+<a href="{{ url_for("profile.start_transition", profile_uuid=profile.uuid, transition=transition) }}" class="btn btn-info">{{ label | capitalize }}</a>
+{% endfor %}
 {% endblock %}
diff --git a/hiboo/profile/templates/profile_list.html b/hiboo/profile/templates/profile_list.html
index ec9436e60b619ca808a17e07af68ae40dd44195e..8f44f2197921b500e7431d5097e83e65cc343ba1 100644
--- a/hiboo/profile/templates/profile_list.html
+++ b/hiboo/profile/templates/profile_list.html
@@ -44,16 +44,9 @@
             <td>{{ macros.profile_status(profile) }}</td>
             <td>{{ profile.created_at.date() }}</td>
             <td>
-              {% if profile.status == "active" %}
-              <a href="{{ url_for("profile.set_status", profile_uuid=profile.uuid, status="blocked") }}">{% trans %}Block profile{% endtrans %}</a>
-              {% elif profile.status == "blocked" %}
-              <a href="{{ url_for("profile.set_status", profile_uuid=profile.uuid, status="active") }}">{% trans %}Unblock profile{% endtrans %}</a>
-              {% elif profile.status == "request" %}
-              <a href="{{ url_for("profile.set_status", profile_uuid=profile.uuid, status="active") }}">{% trans %}Validate profile{% endtrans %}</a>
-              {% elif profile.status == "unclaimed" %}
-              <a href="{{ url_for("profile.assign", profile_uuid=profile.uuid) }}">{% trans %}Assign profile{% endtrans %}</a>
-              {% endif %}
-              <a href="{{ url_for("profile.set_status", profile_uuid=profile.uuid, status="deleted") }}">{% trans %}Delete profile{% endtrans %}</a>
+              {% for transition, (_, _, _, label) in profile.transitions.items() %}
+              <a href="{{ url_for("profile.start_transition", profile_uuid=profile.uuid, transition=transition) }}">{{ label | capitalize }}</a>
+              {% endfor %}
             </td>
           </tr>
           {% endfor %}
diff --git a/hiboo/profile/transition.py b/hiboo/profile/transition.py
deleted file mode 100644
index 846be0efc42999223a2da81ad67d7e039b3a32de..0000000000000000000000000000000000000000
--- a/hiboo/profile/transition.py
+++ /dev/null
@@ -1,45 +0,0 @@
-from hiboo import models
-
-# These are coded as (current, target), None being a wildcard.
-# All matches apply, including wildcards.
-# Values are pairs of lists, first actions being applied before,
-# then actions being applied after the transition.
-TRANSITIONS = {
-    (None, models.Profile.BLOCKED): (
-        ["block"],
-        ["notify"]
-    ),
-    (None, models.Profile.DELETED): (
-        ["delete"],
-        ["notify"]
-    ),
-    (None, models.Profile.ACTIVE): (
-        ["activate"],
-        ["notify"]
-    ),
-    (models.Profile.REQUEST, models.Profile.ACTIVE): (
-        ["welcome"],
-        []
-    ),
-}
-
-
-def apply(profile, current, target, done=False):
-    """ Handle profile transitions as single-step workflows
-    """
-    app = profile.service.application
-    index = 1 if done else 0
-    empty = ([], [])
-    functions = (
-        TRANSITIONS.get((current, target), empty)[index] +
-        TRANSITIONS.get((None, target), empty)[index] +
-        TRANSITIONS.get((current, None), empty)[index]
-    )
-    valid = any([
-        hasattr(app, function) and getattr(app, function)(profile)
-        for function in functions
-    ])
-    if valid and not done:
-        profile.status = profile.transition
-        profile.transition = None
-        transition(profile, current, target, True)
diff --git a/hiboo/profile/views.py b/hiboo/profile/views.py
index 219aac62769613997c93f1fdf9a2daa3a66ea775..052b8529bd20b8e77ad2badc85d0aa82a57d0fd0 100644
--- a/hiboo/profile/views.py
+++ b/hiboo/profile/views.py
@@ -1,4 +1,4 @@
-from hiboo.profile import blueprint, forms, transition
+from hiboo.profile import blueprint, forms, common
 from hiboo import security, models, utils, format
 from hiboo import user as hiboo_user
 from passlib import context, hash
@@ -164,22 +164,23 @@ def details(profile_uuid):
     return flask.render_template("profile_details.html", profile=profile)
 
 
-@blueprint.route("/status/<profile_uuid>/<status>", methods=["GET", "POST"])
+@blueprint.route("/transition/<profile_uuid>/<transition>", methods=["GET", "POST"])
 @security.confirmation_required("change the profile status")
 @security.admin_required()
-def set_status(profile_uuid, status):
+def start_transition(profile_uuid, transition):
     profile = models.Profile.query.get(profile_uuid) or flask.abort(404)
-    profile.transition = status
+    current, target, delay, label = profile.transitions.get(transition) or flask.abort(403)
+    profile.transition = transition
     models.log(
-        category=models.History.STATUS,
-        value=status,
+        category=models.History.TRANSITION,
+        value=transition,
         user=profile.user,
         service=profile.service,
         profile=profile,
         actor=flask_login.current_user,
         public=True
     )
-    transition.apply(profile, profile.status, profile.transition)
+    common.apply_transition(profile, profile.status, target, delay, transition)
     models.db.session.commit()
     flask.flash(_("Profile status change was requested"), "success")
     return flask.redirect(flask.url_for(".details", profile_uuid=profile_uuid))
@@ -189,9 +190,10 @@ def set_status(profile_uuid, status):
 @security.admin_required()
 def complete_transition(profile_uuid):
     profile = models.Profile.query.get(profile_uuid) or flask.abort(404)
-    current, target = profile.status, profile.transition
+    transition = profile.transition
+    current, target, delay, label = profile.TRANSITIONS.get(transition) or flask.abort(403)
     profile.status, profile.transition = target, None
-    transition.apply(profile, current, target, True)
+    common.apply_transition(profile, current, target, delay, transition, "post")
     models.db.session.commit()
     flask.flash(_("Profile status change was completed"), "success")
     return flask.redirect(flask.url_for(".details", profile_uuid=profile_uuid))
diff --git a/hiboo/templates/macros.html b/hiboo/templates/macros.html
index 6450780b7c014c299f73ed975e15e1d1c9cd8ad6..0280e174765f40ebbc35b37f85d361dbf759b7a1 100644
--- a/hiboo/templates/macros.html
+++ b/hiboo/templates/macros.html
@@ -32,12 +32,12 @@
 {% endmacro %}
 
 {% macro profile_status(profile) %}
-  {% set status = profile.STATUSES[profile.status] %}
-  <span class="badge bg-{{ status[0] }}">{{ status[1] }}</span>
+  {% set current = profile.STATUSES[profile.status] %}
+  <span class="badge bg-{{ current[0] }}">{{ current[1] }}</span>
   {% if profile.transition %}
-  {% set transition = profile.STATUSES[profile.transition] %}
+  {% set target = profile.STATUSES[profile.TRANSITIONS[profile.transition][1]] %}
   <i class="fa fa-arrow-right"></i>
-  <span class="badge bg-{{ transition[0] }}">{{ transition[1] }}</span>
+  <span class="badge bg-{{ target[0] }}">{{ target[1] }}</span>
   (<a href="{{ url_for("profile.complete_transition", profile_uuid=profile.uuid) }}">{% trans %}complete manually{% endtrans %}</a>)
   {% endif %}
 {% endmacro %}