diff --git a/circle/common/operations.py b/circle/common/operations.py
index b05dd6d..da985ac 100644
--- a/circle/common/operations.py
+++ b/circle/common/operations.py
@@ -74,7 +74,8 @@ class Operation(object):
             self.check_auth(user)
         self.check_precond()
 
-        activity = self.create_activity(parent=parent_activity, user=user)
+        activity = self.create_activity(
+            parent=parent_activity, user=user, kwargs=kwargs)
 
         return activity, allargs, auxargs
 
@@ -150,7 +151,7 @@ class Operation(object):
             raise PermissionDenied("%s doesn't have the required permissions."
                                    % user)
 
-    def create_activity(self, parent, user):
+    def create_activity(self, parent, user, kwargs):
         raise NotImplementedError
 
     def on_abort(self, activity, error):
@@ -159,6 +160,18 @@ class Operation(object):
         """
         pass
 
+    def get_activity_name(self, kwargs):
+        try:
+            return self.activity_name
+        except AttributeError:
+            try:
+                return self.name._proxy____args[0]  # ewww!
+            except AttributeError:
+                raise ImproperlyConfigured(
+                    "Set Operation.activity_name to an ugettext_nooped "
+                    "string or a create_readable call, or override "
+                    "get_activity_name to create a name dynamically")
+
     def on_commit(self, activity):
         """This method is called when the operation executes successfully.
         """
diff --git a/circle/vm/models/activity.py b/circle/vm/models/activity.py
index 8752bd2..9ee9458 100644
--- a/circle/vm/models/activity.py
+++ b/circle/vm/models/activity.py
@@ -18,6 +18,7 @@
 from __future__ import absolute_import, unicode_literals
 from contextlib import contextmanager
 from logging import getLogger
+from warnings import warn
 
 from celery.signals import worker_ready
 from celery.contrib.abortable import AbortableAsyncResult
@@ -28,7 +29,8 @@ from django.utils import timezone
 from django.utils.translation import ugettext_lazy as _, ugettext_noop
 
 from common.models import (
-    ActivityModel, activitycontextimpl, create_readable, join_activity_code
+    ActivityModel, activitycontextimpl, create_readable, join_activity_code,
+    HumanReadableObject,
 )
 
 from manager.mancelery import celery
@@ -49,6 +51,18 @@ class ActivityInProgressError(Exception):
             self.activity = activity
 
 
+def _normalize_readable_name(name, default=None):
+    if name is None:
+        warn("Set readable_name to a HumanReadableObject",
+             DeprecationWarning, 3)
+        name = default.replace(".", " ")
+
+    if not isinstance(name, HumanReadableObject):
+        name = create_readable(name)
+
+    return name
+
+
 class InstanceActivity(ActivityModel):
     ACTIVITY_CODE_BASE = join_activity_code('vm', 'Instance')
     instance = ForeignKey('Instance', related_name='activity_log',
@@ -75,7 +89,9 @@ class InstanceActivity(ActivityModel):
 
     @classmethod
     def create(cls, code_suffix, instance, task_uuid=None, user=None,
-               concurrency_check=True):
+               concurrency_check=True, readable_name=None):
+
+        readable_name = _normalize_readable_name(readable_name, code_suffix)
         # Check for concurrent activities
         active_activities = instance.activity_log.filter(finished__isnull=True)
         if concurrency_check and active_activities.exists():
@@ -84,11 +100,15 @@ class InstanceActivity(ActivityModel):
         activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix)
         act = cls(activity_code=activity_code, instance=instance, parent=None,
                   resultant_state=None, started=timezone.now(),
+                  readable_name_data=readable_name.to_dict(),
                   task_uuid=task_uuid, user=user)
         act.save()
         return act
 
-    def create_sub(self, code_suffix, task_uuid=None, concurrency_check=True):
+    def create_sub(self, code_suffix, task_uuid=None, concurrency_check=True,
+                   readable_name=None):
+
+        readable_name = _normalize_readable_name(readable_name, code_suffix)
         # Check for concurrent activities
         active_children = self.children.filter(finished__isnull=True)
         if concurrency_check and active_children.exists():
@@ -97,7 +117,8 @@ class InstanceActivity(ActivityModel):
         act = InstanceActivity(
             activity_code=join_activity_code(self.activity_code, code_suffix),
             instance=self.instance, parent=self, resultant_state=None,
-            started=timezone.now(), task_uuid=task_uuid, user=self.user)
+            readable_name_data=readable_name.to_dict(), started=timezone.now(),
+            task_uuid=task_uuid, user=self.user)
         act.save()
         return act
 
@@ -158,10 +179,12 @@ class InstanceActivity(ActivityModel):
 
     @contextmanager
     def sub_activity(self, code_suffix, on_abort=None, on_commit=None,
-                     task_uuid=None, concurrency_check=True):
+                     readable_name=None, task_uuid=None,
+                     concurrency_check=True):
         """Create a transactional context for a nested instance activity.
         """
-        act = self.create_sub(code_suffix, task_uuid, concurrency_check)
+        act = self.create_sub(code_suffix, task_uuid, concurrency_check,
+                              readable_name=readable_name)
         return activitycontextimpl(act, on_abort=on_abort, on_commit=on_commit)
 
 
@@ -195,24 +218,32 @@ class NodeActivity(ActivityModel):
                                    self.node)
 
     @classmethod
-    def create(cls, code_suffix, node, task_uuid=None, user=None):
+    def create(cls, code_suffix, node, task_uuid=None, user=None,
+               readable_name=None):
+
+        readable_name = _normalize_readable_name(readable_name, code_suffix)
         activity_code = join_activity_code(cls.ACTIVITY_CODE_BASE, code_suffix)
         act = cls(activity_code=activity_code, node=node, parent=None,
+                  readable_name_data=readable_name.to_dict(),
                   started=timezone.now(), task_uuid=task_uuid, user=user)
         act.save()
         return act
 
-    def create_sub(self, code_suffix, task_uuid=None):
+    def create_sub(self, code_suffix, task_uuid=None, readable_name=None):
+
+        readable_name = _normalize_readable_name(readable_name, code_suffix)
         act = NodeActivity(
             activity_code=join_activity_code(self.activity_code, code_suffix),
             node=self.node, parent=self, started=timezone.now(),
-            task_uuid=task_uuid, user=self.user)
+            readable_name_data=readable_name.to_dict(), task_uuid=task_uuid,
+            user=self.user)
         act.save()
         return act
 
     @contextmanager
-    def sub_activity(self, code_suffix, task_uuid=None):
-        act = self.create_sub(code_suffix, task_uuid)
+    def sub_activity(self, code_suffix, task_uuid=None, readable_name=None):
+        act = self.create_sub(code_suffix, task_uuid,
+                              readable_name=readable_name)
         return activitycontextimpl(act)
 
 
diff --git a/circle/vm/operations.py b/circle/vm/operations.py
index d83ac77..878609c 100644
--- a/circle/vm/operations.py
+++ b/circle/vm/operations.py
@@ -21,10 +21,11 @@ from re import search
 
 from django.core.exceptions import PermissionDenied
 from django.utils import timezone
-from django.utils.translation import ugettext_lazy as _
+from django.utils.translation import ugettext_lazy as _, ugettext_noop
 
 from celery.exceptions import TimeLimitExceeded
 
+from common.models import create_readable
 from common.operations import Operation, register_operation
 from .tasks.local_tasks import (
     abortable_async_instance_operation, abortable_async_node_operation,
@@ -59,7 +60,8 @@ class InstanceOperation(Operation):
 
         super(InstanceOperation, self).check_auth(user=user)
 
-    def create_activity(self, parent, user):
+    def create_activity(self, parent, user, kwargs):
+        name = self.get_activity_name(kwargs)
         if parent:
             if parent.instance != self.instance:
                 raise ValueError("The instance associated with the specified "
@@ -70,11 +72,13 @@ class InstanceOperation(Operation):
                                  "parent activity does not match the user "
                                  "provided as parameter.")
 
-            return parent.create_sub(code_suffix=self.activity_code_suffix)
+            return parent.create_sub(code_suffix=self.activity_code_suffix,
+                                     readable_name=name)
         else:
             return InstanceActivity.create(
                 code_suffix=self.activity_code_suffix, instance=self.instance,
-                user=user, concurrency_check=self.concurrency_check)
+                readable_name=name, user=user,
+                concurrency_check=self.concurrency_check)
 
     def is_preferred(self):
         """If this is the recommended op in the current state of the instance.
@@ -102,6 +106,10 @@ class AddInterfaceOperation(InstanceOperation):
 
         return net
 
+    def get_activity_name(self, kwargs):
+        return create_readable(ugettext_noop("add %(vlan)s interface"),
+                               vlan=kwargs['vlan'])
+
 
 register_operation(AddInterfaceOperation)
 
@@ -646,7 +654,8 @@ class NodeOperation(Operation):
         super(NodeOperation, self).__init__(subject=node)
         self.node = node
 
-    def create_activity(self, parent, user):
+    def create_activity(self, parent, user, kwargs):
+        name = self.get_activity_name(kwargs)
         if parent:
             if parent.node != self.node:
                 raise ValueError("The node associated with the specified "
@@ -657,10 +666,12 @@ class NodeOperation(Operation):
                                  "parent activity does not match the user "
                                  "provided as parameter.")
 
-            return parent.create_sub(code_suffix=self.activity_code_suffix)
+            return parent.create_sub(code_suffix=self.activity_code_suffix,
+                                     readable_name=name)
         else:
             return NodeActivity.create(code_suffix=self.activity_code_suffix,
-                                       node=self.node, user=user)
+                                       node=self.node, user=user,
+                                       readable_name=name)
 
 
 class FlushOperation(NodeOperation):