diff --git a/platypush/backend/http/webapp/src/components/Action/ActionsBlock.vue b/platypush/backend/http/webapp/src/components/Action/ActionsBlock.vue
index d5f508487..315bc46a6 100644
--- a/platypush/backend/http/webapp/src/components/Action/ActionsBlock.vue
+++ b/platypush/backend/http/webapp/src/components/Action/ActionsBlock.vue
@@ -14,6 +14,7 @@
                      :dragging="dragging"
                      :has-else="hasElse"
                      :indent="indent"
+                     :is-inside-loop="isInsideLoop"
                      :parent="value"
                      :read-only="readOnly"
                      @add-else="$emit('add-else')"
@@ -84,6 +85,11 @@ export default {
       default: 0,
     },
 
+    isInsideLoop: {
+      type: Boolean,
+      default: false,
+    },
+
     readOnly: {
       type: Boolean,
       default: false,
diff --git a/platypush/backend/http/webapp/src/components/Action/ActionsList.vue b/platypush/backend/http/webapp/src/components/Action/ActionsList.vue
index d366e87f6..4efbb453f 100644
--- a/platypush/backend/http/webapp/src/components/Action/ActionsList.vue
+++ b/platypush/backend/http/webapp/src/components/Action/ActionsList.vue
@@ -25,6 +25,24 @@
                         :is-else="true"
                         v-else-if="elses[index]" />
 
+        <LoopBlock v-bind="componentsData[index].props"
+                   v-on="componentsData[index].on"
+                   :collapsed="collapsedBlocks[index]"
+                   :dragging="isDragging"
+                   v-else-if="fors[index]" />
+
+        <BreakTile :active="isDragging"
+                   :readOnly="readOnly"
+                   :spacerTop="componentsData[index].props.spacerTop"
+                   @delete="deleteAction(index)"
+                   v-else-if="isBreak(action)" />
+
+        <ContinueTile :active="isDragging"
+                      :readOnly="readOnly"
+                      :spacerTop="componentsData[index].props.spacerTop"
+                      @delete="deleteAction(index)"
+                      v-else-if="isContinue(action)" />
+
         <ReturnTile v-bind="componentsData[index].props"
                     :value="returnValue"
                     @change="editReturn($event)"
@@ -67,6 +85,18 @@
         <div class="row item action add-else-container" v-if="visibleAddButtons.else">
           <AddTile icon="fas fa-question" title="Add Else" @click="$emit('add-else')" />
         </div>
+
+        <div class="row item action add-for-container" v-if="visibleAddButtons.loop">
+          <AddTile icon="fas fa-arrow-rotate-left" title="Add Loop" @click="addLoop" />
+        </div>
+
+        <div class="row item action add-break-container" v-if="visibleAddButtons.break">
+          <AddTile icon="fas fa-hand" title="Add Break" @click="addBreak" />
+        </div>
+
+        <div class="row item action add-continue-container" v-if="visibleAddButtons.continue">
+          <AddTile icon="fas fa-rotate" title="Add Continue" @click="addContinue" />
+        </div>
       </div>
     </div>
   </div>
@@ -76,8 +106,11 @@
 import ActionsListItem from "./ActionsListItem"
 import ActionTile from "./ActionTile"
 import AddTile from "./AddTile"
+import BreakTile from "./BreakTile"
 import ConditionBlock from "./ConditionBlock"
+import ContinueTile from "./ContinueTile"
 import ListItem from "./ListItem"
+import LoopBlock from "./LoopBlock"
 import Mixin from "./Mixin"
 import ReturnTile from "./ReturnTile"
 import Utils from "@/Utils"
@@ -103,8 +136,11 @@ export default {
     ActionsListItem,
     ActionTile,
     AddTile,
+    BreakTile,
     ConditionBlock,
+    ContinueTile,
     ListItem,
+    LoopBlock,
     ReturnTile,
   },
 
@@ -119,12 +155,17 @@ export default {
       default: false,
     },
 
+    hasElse: {
+      type: Boolean,
+      default: false,
+    },
+
     indent: {
       type: Number,
       default: 0,
     },
 
-    hasElse: {
+    isInsideLoop: {
       type: Boolean,
       default: false,
     },
@@ -187,6 +228,7 @@ export default {
           props: {
             value: action,
             active: this.isDragging,
+            isInsideLoop: !!(this.isInsideLoop || this.getFor(action) || this.getWhile(action)),
             readOnly: this.readOnly,
             ref: `action-tile-${index}`,
             spacerBottom: this.visibleBottomSpacers[index],
@@ -223,6 +265,11 @@ export default {
           data.props.indent = this.indent + 1
         }
 
+        const loop = this.getFor(action)
+        if (loop) {
+          data.props.async = loop.async
+        }
+
         return data
       })
     },
@@ -281,6 +328,16 @@ export default {
       }, {}) || {}
     },
 
+    fors() {
+      return this.newValue?.reduce?.((acc, action, index) => {
+        if (this.getFor(action)) {
+          acc[index] = action
+        }
+
+        return acc
+      }, {}) || {}
+    },
+
     hasChanges() {
       return this.newStringValue !== this.stringValue
     },
@@ -301,18 +358,16 @@ export default {
       return this.dragIndices?.[0]
     },
 
+    breakIndex() {
+      return this.getTileIndex((action) => this.isBreak(action))
+    },
+
+    continueIndex() {
+      return this.getTileIndex((action) => this.isContinue(action))
+    },
+
     returnIndex() {
-      const ret = this.newValue?.reduce?.((acc, action, index) => {
-        if (acc >= 0)
-          return acc
-
-        if (this.isReturn(action))
-          return index
-
-        return acc
-      }, -1)
-
-      return ret >= 0 ? ret : null
+      return this.getTileIndex((action) => this.isReturn(action))
     },
 
     returnValue() {
@@ -349,7 +404,22 @@ export default {
     },
 
     stopIndex() {
-      return this.returnIndex
+      if (this.breakIndex != null)
+        return this.breakIndex
+      if (this.continueIndex != null)
+        return this.continueIndex
+      if (this.returnIndex != null)
+        return this.returnIndex
+
+      return null
+    },
+
+    allowAddButtons() {
+      return (
+        !this.readOnly &&
+        !this.collapsed &&
+        this.stopIndex == null
+      )
     },
 
     visibleActions() {
@@ -360,8 +430,11 @@ export default {
         if (
           this.conditions[index] ||
           this.elses[index] ||
+          this.fors[index] ||
           this.isAction(action) ||
-          this.isReturn(action)
+          this.isReturn(action) ||
+          this.isBreak(action) ||
+          this.isContinue(action)
         ) {
           acc[index] = action
         }
@@ -372,16 +445,23 @@ export default {
 
     visibleAddButtons() {
       return {
-        action: !this.readOnly && !this.collapsed && this.stopIndex == null,
-        return: !this.readOnly && !this.collapsed && this.stopIndex == null,
-        condition: !this.readOnly && !this.collapsed && this.stopIndex == null,
+        action: this.allowAddButtons,
+        return: this.allowAddButtons,
+        condition: this.allowAddButtons,
+        loop: this.allowAddButtons,
         else: (
-          !this.readOnly &&
-          !this.collapsed &&
+          this.allowAddButtons &&
           this.parent &&
           this.getCondition(this.parent) &&
-          !this.hasElse &&
-          this.stopIndex == null
+          !this.hasElse
+        ),
+        break: (
+          this.allowAddButtons &&
+          this.isInsideLoop
+        ),
+        continue: (
+          this.allowAddButtons &&
+          this.isInsideLoop
         ),
       }
     },
@@ -583,6 +663,19 @@ export default {
       this.selectLastExprEditor()
     },
 
+    addLoop() {
+      this.newValue.push({ 'for _ in ${range(10)}': [] })
+      this.selectLastExprEditor()
+    },
+
+    addBreak() {
+      this.newValue.push('break')
+    },
+
+    addContinue() {
+      this.newValue.push('continue')
+    },
+
     addReturn() {
       this.newValue.push({ 'return': null })
       this.selectLastExprEditor()
@@ -606,7 +699,7 @@ export default {
 
         newTileElement.click()
         this.$nextTick(() => {
-          const exprEditor = newTile.$el?.querySelector('.expr-editor-container')
+          const exprEditor = newTile.$el?.querySelector('.editor-container')
           if (!exprEditor) {
             return
           }
@@ -644,6 +737,20 @@ export default {
       }
     },
 
+    getTileIndex(callback) {
+      const ret = this.newValue?.reduce?.((acc, action, index) => {
+        if (acc >= 0)
+          return acc
+
+        if (callback(action))
+          return index
+
+        return acc
+      }, -1)
+
+      return ret >= 0 ? ret : null
+    },
+
     syncSpacers() {
       this.$nextTick(() => {
         this.spacerElements = Object.keys(this.newValue).reduce((acc, index) => {
diff --git a/platypush/backend/http/webapp/src/components/Action/BreakTile.vue b/platypush/backend/http/webapp/src/components/Action/BreakTile.vue
new file mode 100644
index 000000000..4c4fc1cc8
--- /dev/null
+++ b/platypush/backend/http/webapp/src/components/Action/BreakTile.vue
@@ -0,0 +1,66 @@
+<template>
+  <ListItem class="break-tile"
+            value="break"
+            :active="active"
+            :read-only="readOnly"
+            :spacer-bottom="spacerBottom"
+            :spacer-top="spacerTop">
+    <Tile :value="value"
+          class="keyword"
+          :draggable="false"
+          :read-only="readOnly"
+          :with-delete="!readOnly"
+          @click.stop
+          @delete="$emit('delete')">
+      <div class="tile-name">
+        <span class="icon">
+          <i class="fas fa-hand" />
+        </span>
+        <span class="name">
+          <span class="keyword">break</span>
+        </span>
+      </div>
+    </Tile>
+  </ListItem>
+</template>
+
+<script>
+import ListItem from "./ListItem"
+import Tile from "@/components/elements/Tile"
+
+export default {
+  emits: ['delete'],
+
+  components: {
+    ListItem,
+    Tile,
+  },
+
+  props: {
+    value: {
+      type: String,
+      default: 'break',
+    },
+
+    active: {
+      type: Boolean,
+      default: false,
+    },
+
+    readOnly: {
+      type: Boolean,
+      default: false,
+    },
+
+    spacerBottom: {
+      type: Boolean,
+      default: true,
+    },
+
+    spacerTop: {
+      type: Boolean,
+      default: true,
+    },
+  },
+}
+</script>
diff --git a/platypush/backend/http/webapp/src/components/Action/ConditionBlock.vue b/platypush/backend/http/webapp/src/components/Action/ConditionBlock.vue
index d0f6f6e0c..40fb29189 100644
--- a/platypush/backend/http/webapp/src/components/Action/ConditionBlock.vue
+++ b/platypush/backend/http/webapp/src/components/Action/ConditionBlock.vue
@@ -4,6 +4,7 @@
                   :collapsed="collapsed"
                   :dragging="isDragging"
                   :has-else="hasElse"
+                  :is-inside-loop="isInsideLoop"
                   :indent="indent"
                   :read-only="readOnly"
                   @input="onActionsChange"
@@ -107,6 +108,11 @@ export default {
       default: false,
     },
 
+    isInsideLoop: {
+      type: Boolean,
+      default: false,
+    },
+
     readOnly: {
       type: Boolean,
       default: false,
diff --git a/platypush/backend/http/webapp/src/components/Action/ConditionTile.vue b/platypush/backend/http/webapp/src/components/Action/ConditionTile.vue
index 4493b34c3..f33f7c7cb 100644
--- a/platypush/backend/http/webapp/src/components/Action/ConditionTile.vue
+++ b/platypush/backend/http/webapp/src/components/Action/ConditionTile.vue
@@ -21,7 +21,6 @@
         <span class="name">
           <span class="keyword">if</span> [
           <span class="code" v-text="value" /> ]
-          <span class="keyword">then</span>
         </span>
       </div>
     </Tile>
diff --git a/platypush/backend/http/webapp/src/components/Action/ContinueTile.vue b/platypush/backend/http/webapp/src/components/Action/ContinueTile.vue
new file mode 100644
index 000000000..1df7dc865
--- /dev/null
+++ b/platypush/backend/http/webapp/src/components/Action/ContinueTile.vue
@@ -0,0 +1,66 @@
+<template>
+  <ListItem class="continue-tile"
+            value="continue"
+            :active="active"
+            :read-only="readOnly"
+            :spacer-bottom="spacerBottom"
+            :spacer-top="spacerTop">
+    <Tile :value="value"
+          class="keyword"
+          :draggable="false"
+          :read-only="readOnly"
+          :with-delete="!readOnly"
+          @click.stop
+          @delete="$emit('delete')">
+      <div class="tile-name">
+        <span class="icon">
+          <i class="fas fa-rotate" />
+        </span>
+        <span class="name">
+          <span class="keyword">continue</span>
+        </span>
+      </div>
+    </Tile>
+  </ListItem>
+</template>
+
+<script>
+import ListItem from "./ListItem"
+import Tile from "@/components/elements/Tile"
+
+export default {
+  emits: ['delete'],
+
+  components: {
+    ListItem,
+    Tile,
+  },
+
+  props: {
+    value: {
+      type: String,
+      default: 'continue',
+    },
+
+    active: {
+      type: Boolean,
+      default: false,
+    },
+
+    readOnly: {
+      type: Boolean,
+      default: false,
+    },
+
+    spacerBottom: {
+      type: Boolean,
+      default: true,
+    },
+
+    spacerTop: {
+      type: Boolean,
+      default: true,
+    },
+  },
+}
+</script>
diff --git a/platypush/backend/http/webapp/src/components/Action/EndBlockTile.vue b/platypush/backend/http/webapp/src/components/Action/EndBlockTile.vue
index 4ae3cee4a..5e3f56f5b 100644
--- a/platypush/backend/http/webapp/src/components/Action/EndBlockTile.vue
+++ b/platypush/backend/http/webapp/src/components/Action/EndBlockTile.vue
@@ -12,7 +12,7 @@
     <Tile class="keyword" :draggable="false" :read-only="true">
       <div class="tile-name">
         <span class="icon">
-          <i class="fas fa-question" />
+          <i :class="icon" />
         </span>
         <span class="name">
           <span class="keyword" v-text="value" />
diff --git a/platypush/backend/http/webapp/src/components/Action/ExpressionEditor.vue b/platypush/backend/http/webapp/src/components/Action/ExpressionEditor.vue
index 9b1c71ee4..bdf9b9661 100644
--- a/platypush/backend/http/webapp/src/components/Action/ExpressionEditor.vue
+++ b/platypush/backend/http/webapp/src/components/Action/ExpressionEditor.vue
@@ -29,7 +29,7 @@ export default {
 
   props: {
     value: {
-      type: String,
+      type: [String, Number, Boolean, Object, Array],
       default: '',
     },
 
@@ -83,7 +83,7 @@ export default {
 
   mounted() {
     this.hasChanges = false
-    if (!this.value?.trim()?.length) {
+    if (!this.value?.trim?.()?.length) {
       this.hasChanges = this.allowEmpty
     }
 
diff --git a/platypush/backend/http/webapp/src/components/Action/ListItem.vue b/platypush/backend/http/webapp/src/components/Action/ListItem.vue
index f9354f869..e938f5b18 100644
--- a/platypush/backend/http/webapp/src/components/Action/ListItem.vue
+++ b/platypush/backend/http/webapp/src/components/Action/ListItem.vue
@@ -87,7 +87,7 @@ export default {
     },
 
     value: {
-      type: [String, Object],
+      type: [String, Number, Boolean, Object, Array],
       required: true,
     },
   },
diff --git a/platypush/backend/http/webapp/src/components/Action/LoopBlock.vue b/platypush/backend/http/webapp/src/components/Action/LoopBlock.vue
new file mode 100644
index 000000000..56c5ecdc7
--- /dev/null
+++ b/platypush/backend/http/webapp/src/components/Action/LoopBlock.vue
@@ -0,0 +1,204 @@
+<template>
+  <div class="loop-block">
+    <ActionsBlock :value="value"
+                  :collapsed="collapsed"
+                  :dragging="isDragging"
+                  :indent="indent"
+                  :is-inside-loop="true"
+                  :read-only="readOnly"
+                  @input="onActionsChange"
+                  @drag="$emit('drag', $event)"
+                  @dragend="$emit('dragend', $event)"
+                  @dragenter="$emit('dragenter', $event)"
+                  @dragleave="$emit('dragleave', $event)"
+                  @dragover="$emit('dragover', $event)"
+                  @drop="$emit('drop', $event)">
+      <template #before>
+        <LoopTile v-bind="loopTileConf.props"
+                  v-on="loopTileConf.on"
+                  @input.prevent.stop
+                  :spacer-top="spacerTop"
+                  :spacer-bottom="false" />
+      </template>
+
+      <template #after>
+        <EndBlockTile value="end for"
+                      icon="fas fa-arrow-rotate-right"
+                      :active="active"
+                      :spacer-bottom="spacerBottom"
+                      @drop="onDrop" />
+      </template>
+    </ActionsBlock>
+  </div>
+</template>
+
+<script>
+import ActionsBlock from "./ActionsBlock"
+import EndBlockTile from "./EndBlockTile"
+import LoopTile from "./LoopTile"
+import Mixin from "./Mixin"
+
+export default {
+  name: 'LoopBlock',
+  mixins: [Mixin],
+  emits: [
+    'delete',
+    'drag',
+    'dragend',
+    'dragenter',
+    'dragleave',
+    'dragover',
+    'drop',
+    'input',
+  ],
+
+  components: {
+    ActionsBlock,
+    LoopTile,
+    EndBlockTile,
+  },
+
+  props: {
+    value: {
+      type: Object,
+      required: true,
+    },
+
+    active: {
+      type: Boolean,
+      default: false,
+    },
+
+    async: {
+      type: Boolean,
+      default: false,
+    },
+
+    collapsed: {
+      type: Boolean,
+      default: false,
+    },
+
+    dragging: {
+      type: Boolean,
+      default: false,
+    },
+
+    indent: {
+      type: Number,
+      default: 0,
+    },
+
+    isInsideLoop: {
+      type: Boolean,
+      default: true,
+    },
+
+    readOnly: {
+      type: Boolean,
+      default: false,
+    },
+
+    spacerBottom: {
+      type: Boolean,
+      default: false,
+    },
+
+    spacerTop: {
+      type: Boolean,
+      default: false,
+    },
+  },
+
+  data() {
+    return {
+      dragging_: false,
+    }
+  },
+
+  computed: {
+    loop() {
+      return this.getFor(this.key)
+    },
+
+    loopTileConf() {
+      return {
+        props: {
+          ...this.loop,
+          active: this.active,
+          readOnly: this.readOnly,
+          spacerBottom: this.spacerBottom,
+          spacerTop: this.spacerTop,
+        },
+
+        on: {
+          change: this.onLoopChange,
+          delete: (event) => this.$emit('delete', event),
+          drag: this.onDragStart,
+          dragend: this.onDragEnd,
+          dragenterspacer: (event) => this.$emit('dragenter', event),
+          dragleavespacer: (event) => this.$emit('dragleave', event),
+          dragover: (event) => this.$emit('dragover', event),
+          dragoverspacer: (event) => this.$emit('dragoverspacer', event),
+          drop: this.onDrop,
+        },
+      }
+    },
+
+    isDragging() {
+      return this.dragging_ || this.dragging
+    },
+
+    key() {
+      return this.getKey(this.value)
+    },
+  },
+
+  methods: {
+    onActionsChange(value) {
+      if (!this.key || this.readOnly) {
+        return
+      }
+
+      this.$emit('input', { [this.key]: value })
+    },
+
+    onLoopChange(loop) {
+      const iterable = loop?.iterable?.trim()
+      const iterator = loop?.iterator?.trim()
+      const async_ = loop?.async || false
+
+      if (!this.key || this.readOnly || !iterable?.length || !iterator?.length) {
+        return
+      }
+
+      const keyword = 'for' + (async_ ? 'k' : '')
+      loop = `${keyword} ${iterator} in \${${iterable}}`
+      this.$emit('input', { [loop]: this.value[this.key] })
+    },
+
+    onDragStart(event) {
+      if (this.readOnly) {
+        return
+      }
+
+      this.dragging_ = true
+      this.$emit('drag', event)
+    },
+
+    onDragEnd(event) {
+      this.dragging_ = false
+      this.$emit('dragend', event)
+    },
+
+    onDrop(event) {
+      if (this.readOnly) {
+        return
+      }
+
+      this.dragging_ = false
+      this.$emit('drop', event)
+    },
+  },
+}
+</script>
diff --git a/platypush/backend/http/webapp/src/components/Action/LoopEditor.vue b/platypush/backend/http/webapp/src/components/Action/LoopEditor.vue
new file mode 100644
index 000000000..d54803a7d
--- /dev/null
+++ b/platypush/backend/http/webapp/src/components/Action/LoopEditor.vue
@@ -0,0 +1,153 @@
+<template>
+  <form class="loop-editor" @submit.prevent.stop="onSubmit">
+    for
+    <label for="iterator">
+      <input type="text"
+             name="iterator"
+             autocomplete="off"
+             :autofocus="true"
+             placeholder="Iterator"
+             :value="iterator"
+             ref="iterator"
+             @input.stop="onInput('iterator', $event)" />
+    </label>
+
+    in
+
+    <label for="iterable">
+      <input type="text"
+             name="iterable"
+             autocomplete="off"
+             :autofocus="true"
+             placeholder="Iterable"
+             :value="iterable"
+             ref="iterable"
+             @input.stop="onInput('iterable', $event)" />
+    </label>
+
+    <label class="async">
+      <input class="checkbox"
+             type="checkbox"
+             name="async"
+             :checked="async"
+             @input.stop="onInput('async', $event)" />&nbsp;
+        Run in parallel
+    </label>
+
+    <label>
+      <button type="submit" :disabled="!hasChanges">
+        <i class="fas fa-check" />&nbsp;Save
+      </button>
+    </label>
+  </form>
+</template>
+
+<script>
+export default {
+  emits: [
+    'change',
+    'input',
+  ],
+
+  props: {
+    async: {
+      type: Boolean,
+      default: false,
+    },
+
+    iterable: {
+      type: String,
+      default: '',
+    },
+
+    iterator: {
+      type: String,
+      default: '',
+    },
+  },
+
+  data() {
+    return {
+      hasChanges: true,
+    }
+  },
+
+  methods: {
+    onSubmit() {
+      const iterator = this.$refs.iterator.value.trim()
+      const iterable = this.$refs.iterable.value.trim()
+      if (!iterator.length || !iterable.length) {
+        return
+      }
+
+      this.$emit('change', { iterator, iterable })
+    },
+
+    onInput(target, event) {
+      const value = '' + event.target.value
+      if (!value?.trim()?.length) {
+        this.hasChanges = false
+      } else {
+        if (target === 'iterator') {
+          this.hasChanges = value !== this.iterator
+        }
+
+        if (!this.hasChanges && target === 'iterable') {
+          this.hasChanges = value !== this.iterable
+        }
+
+        if (!this.hasChanges && target === 'async') {
+          this.hasChanges = value !== this.async
+        }
+      }
+
+      this.$nextTick(() => {
+        event.target.value = value
+      })
+    },
+  },
+
+  watch: {
+    value() {
+      this.hasChanges = false
+    },
+  },
+
+  mounted() {
+    this.$nextTick(() => {
+      this.$refs.iterator.focus()
+    })
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@import "common";
+
+.loop-editor {
+  min-width: 40em;
+  max-width: 100%;
+  display: flex;
+  flex-direction: column;
+  justify-content: center;
+
+  label {
+    width: 100%;
+    display: flex;
+    flex-direction: row;
+    align-items: center;
+    justify-content: center;
+    padding: 1em;
+
+    input[type="text"] {
+      width: 100%;
+      margin-left: 1em;
+    }
+
+    &.async {
+      justify-content: flex-start;
+      padding-bottom: 0;
+    }
+  }
+}
+</style>
diff --git a/platypush/backend/http/webapp/src/components/Action/LoopTile.vue b/platypush/backend/http/webapp/src/components/Action/LoopTile.vue
new file mode 100644
index 000000000..475bede10
--- /dev/null
+++ b/platypush/backend/http/webapp/src/components/Action/LoopTile.vue
@@ -0,0 +1,212 @@
+<template>
+  <ListItem class="loop-tile"
+            :value="value"
+            :active="active"
+            :read-only="readOnly"
+            :spacer-bottom="spacerBottom"
+            :spacer-top="spacerTop"
+            v-on="dragListeners"
+            @input="$emit('input', $event)">
+    <div class="drag-spacer" v-if="dragging && !spacerTop">&nbsp;</div>
+
+    <Tile v-bind="tileConf.props"
+          v-on="tileConf.on"
+          :draggable="!readOnly"
+          @click.stop="showLoopEditor = true">
+      <div class="tile-name">
+        <span class="icon">
+          <i class="fas fa-arrow-rotate-left" />
+        </span>
+        <span class="name">
+          <span class="keyword">for<span v-if="async">k</span></span>&nbsp;
+          <span class="code" v-text="iterator" />
+          <span class="keyword"> in </span> [
+          <span class="code" v-text="iterable" /> ]
+        </span>
+      </div>
+    </Tile>
+
+    <div class="editor-container" v-if="showLoopEditor && !readOnly">
+      <Modal title="Edit Loop"
+             :visible="true"
+             @close="showLoopEditor = false">
+        <LoopEditor :iterator="iterator"
+                    :iterable="iterable"
+                    :async="async"
+                    ref="loopEditor"
+                    @change="onLoopChange"
+                    v-if="showLoopEditor">
+          Loop
+        </LoopEditor>
+      </Modal>
+    </div>
+  </ListItem>
+</template>
+
+<script>
+import ListItem from "./ListItem"
+import LoopEditor from "./LoopEditor"
+import Modal from "@/components/Modal"
+import Tile from "@/components/elements/Tile"
+
+export default {
+  emits: [
+    'change',
+    'click',
+    'delete',
+    'drag',
+    'dragend',
+    'dragenter',
+    'dragleave',
+    'dragover',
+    'drop',
+    'input',
+  ],
+
+  components: {
+    LoopEditor,
+    ListItem,
+    Modal,
+    Tile,
+  },
+
+  props: {
+    iterator: {
+      type: String,
+      required: true,
+    },
+
+    iterable: {
+      type: String,
+      required: true,
+    },
+
+    active: {
+      type: Boolean,
+      default: false,
+    },
+
+    async: {
+      type: Boolean,
+      default: false,
+    },
+
+    isElse: {
+      type: Boolean,
+      default: false,
+    },
+
+    readOnly: {
+      type: Boolean,
+      default: false,
+    },
+
+    spacerBottom: {
+      type: Boolean,
+      default: true,
+    },
+
+    spacerTop: {
+      type: Boolean,
+      default: true,
+    },
+  },
+
+  computed: {
+    dragListeners() {
+      return this.readOnly ? {} : {
+          drag: this.onDragStart,
+          dragend: this.onDragEnd,
+          dragenter: (event) => this.$emit('dragenter', event),
+          dragleave: (event) => this.$emit('dragleave', event),
+          dragover: (event) => this.$emit('dragover', event),
+          drop: this.onDrop,
+      }
+    },
+
+    tileConf() {
+      return {
+        props: {
+          value: this.value,
+          class: 'keyword',
+          readOnly: this.readOnly,
+          withDelete: !this.readOnly,
+        },
+
+        on: {
+          ...this.dragListeners,
+          delete: () => this.$emit('delete'),
+          input: this.onInput,
+        },
+      }
+    },
+
+    value() {
+      return `for ${this.iterator} in ${this.iterable}`
+    },
+  },
+
+  data() {
+    return {
+      dragging: false,
+      showLoopEditor: false,
+    }
+  },
+
+  methods: {
+    onLoopChange(event) {
+      this.showLoopEditor = false
+      if (this.readOnly) {
+        return
+      }
+
+      this.$emit('change', event)
+    },
+
+    onInput(value) {
+      if (!value || this.readOnly) {
+        return
+      }
+
+      this.$emit('input', value)
+    },
+
+    onDragStart(event) {
+      if (this.readOnly) {
+        return
+      }
+
+      this.dragging = true
+      this.$emit('drag', event)
+    },
+
+    onDragEnd(event) {
+      this.dragging = false
+      this.$emit('dragend', event)
+    },
+
+    onDrop(event) {
+      this.dragging = false
+      if (this.readOnly) {
+        return
+      }
+
+      this.$emit('drop', event)
+    },
+  },
+}
+</script>
+
+<style lang="scss" scoped>
+@import "common";
+
+.action-tile {
+  .condition {
+    font-style: italic;
+  }
+
+  .drag-spacer {
+    height: 0;
+  }
+}
+</style>
diff --git a/platypush/backend/http/webapp/src/components/Action/Mixin.vue b/platypush/backend/http/webapp/src/components/Action/Mixin.vue
index 4911c9a3d..e4f2cc196 100644
--- a/platypush/backend/http/webapp/src/components/Action/Mixin.vue
+++ b/platypush/backend/http/webapp/src/components/Action/Mixin.vue
@@ -6,16 +6,42 @@ export default {
       return value?.trim?.()?.match(/^if\s*\$\{(.*)\}\s*$/i)?.[1]?.trim?.()
     },
 
+    getFor(value) {
+      value = this.getKey(value) || value
+      let m = value?.trim?.()?.match(/^for(k?)\s*(.*)\s*in\s*\$\{(.*)\}\s*$/i)
+      if (!m)
+        return null
+
+      return {
+        async: m[1].length > 0,
+        iterator: m[2].trim(),
+        iterable: m[3].trim(),
+      }
+    },
+
     getKey(value) {
       return this.isKeyword(value) ? Object.keys(value)[0] : null
     },
 
+    getWhile(value) {
+      value = this.getKey(value) || value
+      return value?.trim?.()?.match(/^while\s*\$\{(.*)\}\s*$/i)?.[1]?.trim?.()
+    },
+
     isAction(value) {
       return typeof value === 'object' && !Array.isArray(value) && (value.action || value.name)
     },
 
     isActionsBlock(value) {
-      return this.getCondition(value) || this.isElse(value)
+      return this.getCondition(value) || this.isElse(value) || this.getFor(value)
+    },
+
+    isBreak(value) {
+      return value?.toLowerCase?.()?.trim?.() === 'break'
+    },
+
+    isContinue(value) {
+      return value?.toLowerCase?.()?.trim?.() === 'continue'
     },
 
     isKeyword(value) {
diff --git a/platypush/backend/http/webapp/src/components/Action/Response.vue b/platypush/backend/http/webapp/src/components/Action/Response.vue
index 55ff647ff..93f63b96b 100644
--- a/platypush/backend/http/webapp/src/components/Action/Response.vue
+++ b/platypush/backend/http/webapp/src/components/Action/Response.vue
@@ -32,7 +32,7 @@ export default {
   mixins: [Utils],
   props: {
     response: {
-      type: [String, Object],
+      type: [String, Object, Array, Number, Boolean],
     },
 
     error: {
@@ -51,7 +51,7 @@ export default {
 
     jsonResponse() {
       if (this.isJSON) {
-        return hljs.highlight(this.response, {language: 'json'}).value
+        return hljs.highlight(this.response.toString(), {language: 'json'}).value
       }
 
       return null
diff --git a/platypush/backend/http/webapp/src/components/Action/ReturnTile.vue b/platypush/backend/http/webapp/src/components/Action/ReturnTile.vue
index a4fce9f21..cd9134c27 100644
--- a/platypush/backend/http/webapp/src/components/Action/ReturnTile.vue
+++ b/platypush/backend/http/webapp/src/components/Action/ReturnTile.vue
@@ -19,7 +19,7 @@
       </div>
     </Tile>
 
-    <div class="expr-editor-container" v-if="showExprEditor && !readOnly">
+    <div class="editor-container" v-if="showExprEditor && !readOnly">
       <Modal title="Edit Return"
              :visible="true"
              @close="showExprEditor = false">
@@ -59,7 +59,7 @@ export default {
 
   props: {
     value: {
-      type: String,
+      type: [String, Number, Boolean, Object, Array],
       default: '',
     },
 
@@ -68,11 +68,6 @@ export default {
       default: false,
     },
 
-    isElse: {
-      type: Boolean,
-      default: false,
-    },
-
     readOnly: {
       type: Boolean,
       default: false,
diff --git a/platypush/backend/http/webapp/src/components/panels/Entities/Procedure.vue b/platypush/backend/http/webapp/src/components/panels/Entities/Procedure.vue
index 69dac41ca..f02715c96 100644
--- a/platypush/backend/http/webapp/src/components/panels/Entities/Procedure.vue
+++ b/platypush/backend/http/webapp/src/components/panels/Entities/Procedure.vue
@@ -126,7 +126,7 @@
 
         <div class="info-body" v-if="!infoCollapsed">
           <div class="item">
-            <div class="label">Type</div>
+            <div class="label">Source</div>
             <div class="value">
               <i :class="procedureTypeIconClass" />&nbsp;
               {{ value.procedure_type }}
@@ -323,7 +323,10 @@ export default {
         return 'fab fa-python'
 
       if (this.value.procedure_type === 'config')
-        return 'fas fa-rectangle-list'
+        return 'fas fa-file'
+
+      if (this.value.procedure_type === 'db')
+        return 'fas fa-database'
 
       return this.defaultIconClass
     },
diff --git a/platypush/backend/http/webapp/src/utils/Text.vue b/platypush/backend/http/webapp/src/utils/Text.vue
index 67dfd9d01..c87ebde28 100644
--- a/platypush/backend/http/webapp/src/utils/Text.vue
+++ b/platypush/backend/http/webapp/src/utils/Text.vue
@@ -37,6 +37,16 @@ export default {
     formatNumber(number) {
       return number.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",")
     },
+
+    escapeHTML(value) {
+      return value
+         ?.toString?.()
+         ?.replace?.(/&/g, "&amp;")
+         ?.replace?.(/</g, "&lt;")
+         ?.replace?.(/>/g, "&gt;")
+         ?.replace?.(/"/g, "&quot;")
+         ?.replace?.(/'/g, "&#039;") || ''
+     },
   },
 }
 </script>