diff --git a/platypush/backend/http/webapp/src/components/panels/Execute/Index.vue b/platypush/backend/http/webapp/src/components/panels/Execute/Index.vue
index 2700ddf67..52a9a231f 100644
--- a/platypush/backend/http/webapp/src/components/panels/Execute/Index.vue
+++ b/platypush/backend/http/webapp/src/components/panels/Execute/Index.vue
@@ -30,8 +30,10 @@
               Action documentation
             </div>
 
-            <div class="doc html" v-html="selectedDoc" v-if="htmlDoc" />
-            <div class="doc raw" v-text="selectedDoc" v-else />
+            <div class="doc html">
+              <Loading v-if="docLoading" />
+              <span v-html="selectedDoc" v-else />
+            </div>
           </div>
 
           <div class="options" v-if="action.name in actions && (Object.keys(action.args).length ||
@@ -51,8 +53,10 @@
                     Attribute: <div class="attr-name" v-text="selectedAttr" />
                   </div>
 
-                  <div class="doc html" v-html="selectedAttrDoc" v-if="htmlDoc" />
-                  <div class="doc raw" v-text="selectedAttrDoc" v-else />
+                  <div class="doc html">
+                    <Loading v-if="docLoading" />
+                    <span v-html="selectedAttrDoc" v-else />
+                  </div>
                 </div>
               </div>
 
@@ -87,15 +91,17 @@
                 Attribute: <div class="attr-name" v-text="selectedAttr" />
               </div>
 
-              <div class="doc html" v-html="selectedAttrDoc" v-if="htmlDoc" />
-              <div class="doc raw" v-text="selectedAttrDoc" v-else />
+              <div class="doc html">
+                <Loading v-if="docLoading" />
+                <span v-html="selectedAttrDoc" v-else />
+              </div>
             </div>
+          </div>
 
-            <div class="output-container">
-              <div class="title" v-text="error != null ? 'Error' : 'Output'" v-if="error != null || response != null" />
-              <div class="response" v-html="response" v-if="response != null" />
-              <div class="error" v-html="error" v-else-if="error != null" />
-            </div>
+          <div class="output-container">
+            <div class="title" v-text="error != null ? 'Error' : 'Output'" v-if="error != null || response != null" />
+            <div class="response" v-html="response" v-if="response != null" />
+            <div class="error" v-html="error" v-else-if="error != null" />
           </div>
         </div>
 
@@ -163,6 +169,7 @@ export default {
     return {
       loading: false,
       running: false,
+      docLoading: false,
       structuredInput: true,
       actionChanged: false,
       selectedDoc: undefined,
@@ -175,11 +182,11 @@ export default {
 
       response: undefined,
       error: undefined,
-      htmlDoc: false,
       rawRequest: undefined,
       actions: {},
       plugins: {},
       procedures: {},
+      actionDocsCache: {},
       action: {
         name: undefined,
         args: {},
@@ -195,15 +202,12 @@ export default {
 
       try {
         this.procedures = await this.request('inspect.get_procedures')
-        this.plugins = await this.request('inspect.get_all_plugins', {html_doc: false})
+        this.plugins = await this.request('inspect.get_all_plugins')
       } finally {
         this.loading = false
       }
 
       for (const plugin of Object.values(this.plugins)) {
-        if (plugin.html_doc)
-          this.htmlDoc = true
-
         for (const action of Object.values(plugin.actions)) {
           action.name = plugin.name + '.' + action.name
           action.supportsExtraArgs = !!action.has_kwargs
@@ -213,20 +217,20 @@ export default {
       }
 
       const self = this
-      autocomplete(this.$refs.actionName, Object.keys(this.actions).sort(), (evt, value) => {
+      autocomplete(this.$refs.actionName, Object.keys(this.actions).sort(), (_, value) => {
         this.action.name = value
         self.updateAction()
       })
     },
 
-    updateAction() {
+    async updateAction() {
       if (!(this.action.name in this.actions))
         this.selectedDoc = undefined
 
       if (!this.actionChanged || !(this.action.name in this.actions))
         return
 
-      this.loading = true
+      this.docLoading = true
       try {
         this.action = {
           ...this.actions[this.action.name],
@@ -241,32 +245,27 @@ export default {
           extraArgs: [],
         }
       } finally {
-        this.loading = false
+        this.docLoading = false
       }
 
-      this.selectedDoc = this.parseDoc(this.action.doc)
+      this.selectedDoc =
+        this.actionDocsCache[this.action.name]?.html ||
+        await this.parseDoc(this.action.doc)
+
+      if (!this.actionDocsCache[this.action.name])
+        this.actionDocsCache[this.action.name] = {}
+      this.actionDocsCache[this.action.name].html = this.selectedDoc
+
       this.actionChanged = false
       this.response = undefined
       this.error = undefined
     },
 
-    parseDoc(docString) {
-      if (!docString?.length || this.htmlDoc)
+    async parseDoc(docString) {
+      if (!docString?.length)
         return docString
 
-      let lineNo = 0
-      let trailingSpaces = 0
-
-      return docString.split('\n').reduce((doc, line) => {
-        if (++lineNo === 2)
-          trailingSpaces = line.match(/^(\s*)/)[1].length
-
-        if (line.trim().startsWith('.. code-block'))
-          return doc
-
-        doc += line.slice(trailingSpaces).replaceAll('``', '') + '\n'
-        return doc
-      }, '')
+      return await this.request('utils.rst_to_html', {text: docString})
     },
 
     updateProcedure(name, event) {
@@ -308,11 +307,16 @@ export default {
       this.action.extraArgs.pop(i)
     },
 
-    selectAttrDoc(name) {
-      this.response = undefined
-      this.error = undefined
+    async selectAttrDoc(name) {
       this.selectedAttr = name
-      this.selectedAttrDoc = this.parseDoc(this.action.args[name].doc)
+      this.selectedAttrDoc =
+        this.actionDocsCache[this.action.name]?.[name]?.html ||
+        await this.parseDoc(this.action.args[name].doc)
+
+      if (!this.actionDocsCache[this.action.name])
+        this.actionDocsCache[this.action.name] = {}
+
+      this.actionDocsCache[this.action.name][name] = {html: this.selectedAttrDoc}
     },
 
     resetAttrDoc() {
@@ -450,6 +454,7 @@ $params-tablet-width: 20em;
   }
 
   .action-form {
+    background: $default-bg-2;
     padding: 1em .5em;
   }
 
@@ -544,12 +549,26 @@ $params-tablet-width: 20em;
       display: flex;
       margin-bottom: .5em;
 
+      label {
+        margin-left: 0.25em;
+      }
+
+      .buttons {
+        display: flex;
+        flex-grow: 1;
+        justify-content: right;
+      }
+
       .action-extra-param-del {
         border: 0;
         text-align: right;
         padding: 0 .5em;
       }
 
+      input[type=text] {
+        width: 100%;
+      }
+
       .buttons {
         display: flex;
         align-items: center;
@@ -569,11 +588,6 @@ $params-tablet-width: 20em;
     .doc-container,
     .output-container {
       margin-top: .5em;
-      .doc {
-        &.raw {
-          white-space: pre;
-        }
-      }
     }
 
     .output-container {
@@ -590,7 +604,6 @@ $params-tablet-width: 20em;
       }
 
       .doc {
-        white-space: pre-line;
         width: 100%;
         overflow: auto;
       }
@@ -613,11 +626,6 @@ $params-tablet-width: 20em;
     .attr-doc-container {
       .doc {
         padding: 1em !important;
-
-        &.raw {
-          font-family: monospace;
-          font-size: .8em;
-        }
       }
     }
 
@@ -747,11 +755,14 @@ $params-tablet-width: 20em;
   }
 
   .run-btn {
-    border-radius: 2em;
-    padding: .5em .75em;
+    background: none;
+    border-radius: .25em;
+    padding: .5em 1.5em;
+    box-shadow: $primary-btn-shadow;
 
     &:hover {
-      opacity: .8;
+      background: $hover-bg;
+      box-shadow: none;
     }
   }
 }
diff --git a/platypush/backend/http/webapp/src/components/panels/Execute/vars.scss b/platypush/backend/http/webapp/src/components/panels/Execute/vars.scss
index 76cd76d61..c120a666c 100644
--- a/platypush/backend/http/webapp/src/components/panels/Execute/vars.scss
+++ b/platypush/backend/http/webapp/src/components/panels/Execute/vars.scss
@@ -7,7 +7,7 @@ $response-bg: #edfff2;
 $response-border: 1px dashed #98ff98;
 $error-bg: #ffbcbc;
 $error-border: 1px dashed #ff5353;
-$doc-bg: #e8feff;
-$doc-border: 1px dashed #84f9ff;
+$doc-bg: $background-color;
+$doc-border: 1px dashed $border-color-2;
 $procedure-submit-btn-bg: #ebffeb;
 $section-title-bg: rgba(0, 0, 0, .04);
diff --git a/platypush/backend/http/webapp/src/style/themes/light.scss b/platypush/backend/http/webapp/src/style/themes/light.scss
index a148852de..3810ab257 100644
--- a/platypush/backend/http/webapp/src/style/themes/light.scss
+++ b/platypush/backend/http/webapp/src/style/themes/light.scss
@@ -56,6 +56,7 @@ $border-shadow-right: 2.5px 0 4px 0 $default-shadow-color;
 $border-shadow-bottom-right: 2.5px 2.5px 3px 0 $default-shadow-color;
 $header-shadow: 0px 1px 3px 1px #bbb !default;
 $group-shadow: 3px -2px 6px 1px #98b0a0;
+$primary-btn-shadow: 1px 1px 0.5px 0.75px #32b64640 !default;
 
 //// Modals
 $modal-header-bg: #e0e0e0 !default;
@@ -81,6 +82,7 @@ $default-hover-fg: #35b870 !default;
 $default-hover-fg-2: #38cf80 !default;
 $hover-fg: $default-hover-fg !default;
 $hover-bg: linear-gradient(90deg, rgba(190,246,218,1) 0%, rgba(229,251,240,1) 100%) !default;
+$hover-bg-2: rgb(190,246,218) !default;
 $active-bg: #8fefb7 !default;
 
 /// Disabled
diff --git a/platypush/plugins/inspect/__init__.py b/platypush/plugins/inspect/__init__.py
index ccbb34dc0..abac0df1c 100644
--- a/platypush/plugins/inspect/__init__.py
+++ b/platypush/plugins/inspect/__init__.py
@@ -36,7 +36,6 @@ class InspectPlugin(Plugin):
         self._backends_lock = threading.RLock()
         self._events_lock = threading.RLock()
         self._responses_lock = threading.RLock()
-        self._html_doc = False
 
     def _get_modules(self, parent_class: type):
         for mf_file in scan_manifests(parent_class):
@@ -57,9 +56,7 @@ class InspectPlugin(Plugin):
         for module in self._get_modules(Plugin):
             plugin_name = '.'.join(module.__name__.split('.')[2:])
             plugin_class = get_plugin_class_by_name(plugin_name)
-            model = PluginModel(
-                plugin=plugin_class, prefix=prefix, html_doc=self._html_doc
-            )
+            model = PluginModel(plugin=plugin_class, prefix=prefix)
 
             if model.name:
                 self._plugins[model.name] = model
@@ -70,9 +67,7 @@ class InspectPlugin(Plugin):
         for module in self._get_modules(Backend):
             for _, obj in inspect.getmembers(module):
                 if inspect.isclass(obj) and issubclass(obj, Backend):
-                    model = BackendModel(
-                        backend=obj, prefix=prefix, html_doc=self._html_doc
-                    )
+                    model = BackendModel(backend=obj, prefix=prefix)
                     if model.name:
                         self._backends[model.name] = model
 
@@ -85,9 +80,7 @@ class InspectPlugin(Plugin):
                     continue
 
                 if inspect.isclass(obj) and issubclass(obj, Event) and obj != Event:
-                    event = EventModel(
-                        event=obj, html_doc=self._html_doc, prefix=prefix
-                    )
+                    event = EventModel(event=obj, prefix=prefix)
                     if event.package not in self._events:
                         self._events[event.package] = {event.name: event}
                     else:
@@ -106,24 +99,19 @@ class InspectPlugin(Plugin):
                     and issubclass(obj, Response)
                     and obj != Response
                 ):
-                    response = ResponseModel(
-                        response=obj, html_doc=self._html_doc, prefix=prefix
-                    )
+                    response = ResponseModel(response=obj, prefix=prefix)
                     if response.package not in self._responses:
                         self._responses[response.package] = {response.name: response}
                     else:
                         self._responses[response.package][response.name] = response
 
     @action
-    def get_all_plugins(self, html_doc: Optional[bool] = None):
+    def get_all_plugins(self):
         """
-        :param html_doc: If True then the docstring will be parsed into HTML (default: False)
+        Get information about all the available plugins.
         """
         with self._plugins_lock:
-            if not self._plugins or (
-                html_doc is not None and html_doc != self._html_doc
-            ):
-                self._html_doc = html_doc
+            if not self._plugins:
                 self._init_plugins()
 
             return json.dumps(
@@ -131,15 +119,12 @@ class InspectPlugin(Plugin):
             )
 
     @action
-    def get_all_backends(self, html_doc: Optional[bool] = None):
+    def get_all_backends(self):
         """
-        :param html_doc: If True then the docstring will be parsed into HTML (default: False)
+        Get information about all the available backends.
         """
         with self._backends_lock:
-            if not self._backends or (
-                html_doc is not None and html_doc != self._html_doc
-            ):
-                self._html_doc = html_doc
+            if not self._backends:
                 self._init_backends()
 
             return json.dumps(
@@ -147,15 +132,12 @@ class InspectPlugin(Plugin):
             )
 
     @action
-    def get_all_events(self, html_doc: Optional[bool] = None):
+    def get_all_events(self):
         """
-        :param html_doc: If True then the docstring will be parsed into HTML (default: False)
+        Get information about all the available events.
         """
         with self._events_lock:
-            if not self._events or (
-                html_doc is not None and html_doc != self._html_doc
-            ):
-                self._html_doc = html_doc
+            if not self._events:
                 self._init_events()
 
             return json.dumps(
@@ -166,15 +148,12 @@ class InspectPlugin(Plugin):
             )
 
     @action
-    def get_all_responses(self, html_doc: Optional[bool] = None):
+    def get_all_responses(self):
         """
-        :param html_doc: If True then the docstring will be parsed into HTML (default: False)
+        Get information about all the available responses.
         """
         with self._responses_lock:
-            if not self._responses or (
-                html_doc is not None and html_doc != self._html_doc
-            ):
-                self._html_doc = html_doc
+            if not self._responses:
                 self._init_responses()
 
             return json.dumps(
diff --git a/platypush/plugins/inspect/_model.py b/platypush/plugins/inspect/_model.py
index b3e10d6b6..12824d432 100644
--- a/platypush/plugins/inspect/_model.py
+++ b/platypush/plugins/inspect/_model.py
@@ -2,7 +2,6 @@ from abc import ABC, abstractmethod
 import inspect
 import json
 import re
-from typing import Optional
 
 from platypush.utils import get_decorators
 
@@ -14,16 +13,6 @@ class Model(ABC):
     def __repr__(self):
         return json.dumps(dict(self))
 
-    @staticmethod
-    def to_html(doc):
-        try:
-            import docutils.core  # type: ignore
-        except ImportError:
-            # docutils not found
-            return doc
-
-        return docutils.core.publish_parts(doc, writer_name='html')['html_body']
-
     @abstractmethod
     def __iter__(self):
         raise NotImplementedError()
@@ -40,42 +29,29 @@ class ProcedureEncoder(json.JSONEncoder):
 
 
 class BackendModel(Model):
-    def __init__(self, backend, prefix='', html_doc: Optional[bool] = False):
+    def __init__(self, backend, prefix=''):
         self.name = backend.__module__[len(prefix) :]
-        self.html_doc = html_doc
-        self.doc = (
-            self.to_html(backend.__doc__)
-            if html_doc and backend.__doc__
-            else backend.__doc__
-        )
+        self.doc = backend.__doc__
 
     def __iter__(self):
-        for attr in ['name', 'doc', 'html_doc']:
+        for attr in ['name', 'doc']:
             yield attr, getattr(self, attr)
 
 
 class PluginModel(Model):
-    def __init__(self, plugin, prefix='', html_doc: Optional[bool] = False):
-        self.name = plugin.__module__[len(prefix) :]
-        self.html_doc = html_doc
-        self.doc = (
-            self.to_html(plugin.__doc__)
-            if html_doc and plugin.__doc__
-            else plugin.__doc__
-        )
+    def __init__(self, plugin, prefix=''):
+        self.name = re.sub(r'\._plugin$', '', plugin.__module__[len(prefix) :])
+        self.doc = plugin.__doc__
         self.actions = {
-            action_name: ActionModel(
-                getattr(plugin, action_name), html_doc=html_doc or False
-            )
+            action_name: ActionModel(getattr(plugin, action_name))
             for action_name in get_decorators(plugin, climb_class_hierarchy=True).get(
                 'action', []
             )
         }
 
     def __iter__(self):
-        for attr in ['name', 'actions', 'doc', 'html_doc']:
+        for attr in ['name', 'actions', 'doc']:
             if attr == 'actions':
-                # noinspection PyShadowingNames
                 yield attr, {
                     name: dict(action) for name, action in self.actions.items()
                 },
@@ -84,40 +60,31 @@ class PluginModel(Model):
 
 
 class EventModel(Model):
-    def __init__(self, event, prefix='', html_doc: Optional[bool] = False):
+    def __init__(self, event, prefix=''):
         self.package = event.__module__[len(prefix) :]
         self.name = event.__name__
-        self.html_doc = html_doc
-        self.doc = (
-            self.to_html(event.__doc__) if html_doc and event.__doc__ else event.__doc__
-        )
+        self.doc = event.__doc__
 
     def __iter__(self):
-        for attr in ['name', 'doc', 'html_doc']:
+        for attr in ['name', 'doc']:
             yield attr, getattr(self, attr)
 
 
 class ResponseModel(Model):
-    def __init__(self, response, prefix='', html_doc: Optional[bool] = False):
+    def __init__(self, response, prefix=''):
         self.package = response.__module__[len(prefix) :]
         self.name = response.__name__
-        self.html_doc = html_doc
-        self.doc = (
-            self.to_html(response.__doc__)
-            if html_doc and response.__doc__
-            else response.__doc__
-        )
+        self.doc = response.__doc__
 
     def __iter__(self):
-        for attr in ['name', 'doc', 'html_doc']:
+        for attr in ['name', 'doc']:
             yield attr, getattr(self, attr)
 
 
 class ActionModel(Model):
-    # noinspection PyShadowingNames
-    def __init__(self, action, html_doc: bool = False):
+    def __init__(self, action):
         self.name = action.__name__
-        self.doc, argsdoc = self._parse_docstring(action.__doc__, html_doc=html_doc)
+        self.doc, argsdoc = self._parse_docstring(action.__doc__)
         self.args = {}
         self.has_kwargs = False
 
@@ -134,7 +101,7 @@ class ActionModel(Model):
             }
 
     @classmethod
-    def _parse_docstring(cls, docstring: str, html_doc: bool = False):
+    def _parse_docstring(cls, docstring: str):
         new_docstring = ''
         params = {}
         cur_param = None
@@ -147,11 +114,7 @@ class ActionModel(Model):
             m = re.match(r'^\s*:param ([^:]+):\s*(.*)', line)
             if m:
                 if cur_param:
-                    params[cur_param] = (
-                        cls.to_html(cur_param_docstring)
-                        if html_doc
-                        else cur_param_docstring
-                    )
+                    params[cur_param] = cur_param_docstring
 
                 cur_param = m.group(1)
                 cur_param_docstring = m.group(2)
@@ -160,11 +123,7 @@ class ActionModel(Model):
             else:
                 if cur_param:
                     if not line.strip():
-                        params[cur_param] = (
-                            cls.to_html(cur_param_docstring)
-                            if html_doc
-                            else cur_param_docstring
-                        )
+                        params[cur_param] = cur_param_docstring
                         cur_param = None
                         cur_param_docstring = ''
                     else:
@@ -173,14 +132,9 @@ class ActionModel(Model):
                     new_docstring += line.rstrip() + '\n'
 
         if cur_param:
-            params[cur_param] = (
-                cls.to_html(cur_param_docstring) if html_doc else cur_param_docstring
-            )
+            params[cur_param] = cur_param_docstring
 
-        return (
-            new_docstring.strip() if not html_doc else cls.to_html(new_docstring),
-            params,
-        )
+        return new_docstring.strip(), params
 
     def __iter__(self):
         for attr in ['name', 'args', 'doc', 'has_kwargs']: