diff --git a/package-lock.json b/package-lock.json
index 38ed8820300cd8a55be6eb5599ca2aabebadad01..4a8df54652d28d83f9db907513b3e329ce6214ce 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -13,6 +13,7 @@
         "@aws-sdk/s3-request-presigner": "^3.290.0",
         "@fortawesome/fontawesome-free": "^6.3.0",
         "@popperjs/core": "^2.11.6",
+        "ajv": "^8.12.0",
         "bootstrap": "^5.2.3",
         "dayjs": "^1.11.7",
         "dompurify": "^3.0.1",
@@ -2237,6 +2238,28 @@
         "url": "https://opencollective.com/eslint"
       }
     },
+    "node_modules/@eslint/eslintrc/node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
+    "node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
     "node_modules/@fortawesome/fontawesome-free": {
       "version": "6.3.0",
       "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-6.3.0.tgz",
@@ -2837,14 +2860,13 @@
       }
     },
     "node_modules/ajv": {
-      "version": "6.12.6",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-      "dev": true,
+      "version": "8.12.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+      "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
       "dependencies": {
         "fast-deep-equal": "^3.1.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2",
         "uri-js": "^4.2.2"
       },
       "funding": {
@@ -3901,6 +3923,22 @@
         "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
       }
     },
+    "node_modules/eslint/node_modules/ajv": {
+      "version": "6.12.6",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+      "dev": true,
+      "dependencies": {
+        "fast-deep-equal": "^3.1.1",
+        "fast-json-stable-stringify": "^2.0.0",
+        "json-schema-traverse": "^0.4.1",
+        "uri-js": "^4.2.2"
+      },
+      "funding": {
+        "type": "github",
+        "url": "https://github.com/sponsors/epoberezkin"
+      }
+    },
     "node_modules/eslint/node_modules/eslint-scope": {
       "version": "7.1.1",
       "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
@@ -3923,6 +3961,12 @@
         "node": ">=4.0"
       }
     },
+    "node_modules/eslint/node_modules/json-schema-traverse": {
+      "version": "0.4.1",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+      "dev": true
+    },
     "node_modules/espree": {
       "version": "9.4.0",
       "resolved": "https://registry.npmjs.org/espree/-/espree-9.4.0.tgz",
@@ -4016,8 +4060,7 @@
     "node_modules/fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
-      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-      "dev": true
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
     },
     "node_modules/fast-diff": {
       "version": "1.2.0",
@@ -4914,10 +4957,9 @@
       }
     },
     "node_modules/json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-      "dev": true
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
     },
     "node_modules/json-stable-stringify-without-jsonify": {
       "version": "1.0.1",
@@ -5672,7 +5714,6 @@
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
       "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
-      "dev": true,
       "engines": {
         "node": ">=6"
       }
@@ -5777,6 +5818,14 @@
         "url": "https://github.com/sponsors/mysticatea"
       }
     },
+    "node_modules/require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+      "engines": {
+        "node": ">=0.10.0"
+      }
+    },
     "node_modules/resolve": {
       "version": "1.22.1",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@@ -6329,7 +6378,6 @@
       "version": "4.4.1",
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
       "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
-      "dev": true,
       "dependencies": {
         "punycode": "^2.1.0"
       }
@@ -8596,6 +8644,26 @@
         "js-yaml": "^4.1.0",
         "minimatch": "^3.1.2",
         "strip-json-comments": "^3.1.1"
+      },
+      "dependencies": {
+        "ajv": {
+          "version": "6.12.6",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+          "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
+        }
       }
     },
     "@fortawesome/fontawesome-free": {
@@ -9040,14 +9108,13 @@
       "requires": {}
     },
     "ajv": {
-      "version": "6.12.6",
-      "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
-      "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
-      "dev": true,
+      "version": "8.12.0",
+      "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.12.0.tgz",
+      "integrity": "sha512-sRu1kpcO9yLtYxBKvqfTeh9KzZEwO3STyX1HT+4CaDzC6HpTGYhIhPIzj9XuKU7KYDwnaeh5hcOwjy1QuJzBPA==",
       "requires": {
         "fast-deep-equal": "^3.1.1",
-        "fast-json-stable-stringify": "^2.0.0",
-        "json-schema-traverse": "^0.4.1",
+        "json-schema-traverse": "^1.0.0",
+        "require-from-string": "^2.0.2",
         "uri-js": "^4.2.2"
       }
     },
@@ -9656,6 +9723,18 @@
         "text-table": "^0.2.0"
       },
       "dependencies": {
+        "ajv": {
+          "version": "6.12.6",
+          "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
+          "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
+          "dev": true,
+          "requires": {
+            "fast-deep-equal": "^3.1.1",
+            "fast-json-stable-stringify": "^2.0.0",
+            "json-schema-traverse": "^0.4.1",
+            "uri-js": "^4.2.2"
+          }
+        },
         "eslint-scope": {
           "version": "7.1.1",
           "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.1.1.tgz",
@@ -9671,6 +9750,12 @@
           "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz",
           "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==",
           "dev": true
+        },
+        "json-schema-traverse": {
+          "version": "0.4.1",
+          "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
+          "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
+          "dev": true
         }
       }
     },
@@ -9808,8 +9893,7 @@
     "fast-deep-equal": {
       "version": "3.1.3",
       "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
-      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
-      "dev": true
+      "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
     },
     "fast-diff": {
       "version": "1.2.0",
@@ -10438,10 +10522,9 @@
       }
     },
     "json-schema-traverse": {
-      "version": "0.4.1",
-      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
-      "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
-      "dev": true
+      "version": "1.0.0",
+      "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
+      "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="
     },
     "json-stable-stringify-without-jsonify": {
       "version": "1.0.1",
@@ -10981,8 +11064,7 @@
     "punycode": {
       "version": "2.1.1",
       "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz",
-      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==",
-      "dev": true
+      "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A=="
     },
     "queue-microtask": {
       "version": "1.2.3",
@@ -11048,6 +11130,11 @@
       "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==",
       "dev": true
     },
+    "require-from-string": {
+      "version": "2.0.2",
+      "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+      "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="
+    },
     "resolve": {
       "version": "1.22.1",
       "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.1.tgz",
@@ -11443,7 +11530,6 @@
       "version": "4.4.1",
       "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
       "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
-      "dev": true,
       "requires": {
         "punycode": "^2.1.0"
       }
diff --git a/package.json b/package.json
index 1460ccd60a37d0ea736c567205c59be9805f573c..9480d9a3f0588cdfd80db0867fe8ffc9214fd4e7 100644
--- a/package.json
+++ b/package.json
@@ -18,6 +18,7 @@
     "@aws-sdk/s3-request-presigner": "^3.290.0",
     "@fortawesome/fontawesome-free": "^6.3.0",
     "@popperjs/core": "^2.11.6",
+    "ajv": "^8.12.0",
     "bootstrap": "^5.2.3",
     "dayjs": "^1.11.7",
     "dompurify": "^3.0.1",
diff --git a/src/App.vue b/src/App.vue
index 359fecb2daa41b1f0944b576b527f521dd287805..5a48cd877e574e7149407d4eb83051985c4bee4f 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -44,7 +44,7 @@ onBeforeMount(() => {
 
 <template>
   <NavbarTop />
-  <div class="container mt-4">
+  <div class="container-xxl mt-4">
     <router-view></router-view>
   </div>
 </template>
diff --git a/src/assets/main.css b/src/assets/main.css
index 7024e78b25fc6d1c60c77ff425523d2a21fca9e4..8237a72ed8cd830c4dfca825a3cc69eb7ed07564 100644
--- a/src/assets/main.css
+++ b/src/assets/main.css
@@ -14,3 +14,13 @@ body {
 .cursor-pointer {
     cursor: pointer;
 }
+
+.helpTextCode > pre {
+    border: thin solid var(--bs-secondary);
+    border-radius: var(--bs-border-radius);
+    background: var(--bs-dark);
+    filter: brightness(0.9);
+    padding: 1rem;
+    margin: 1em 2em;
+    color: var(--bs-code-color);
+}
diff --git a/src/components/object-storage/modals/CreateBucketModal.vue b/src/components/object-storage/modals/CreateBucketModal.vue
index 83b0f554de64265e8785fbb4b9eb09a7fe7dde6c..69d625863ec8dc4fc61d7bca107bd85c08930dd4 100644
--- a/src/components/object-storage/modals/CreateBucketModal.vue
+++ b/src/components/object-storage/modals/CreateBucketModal.vue
@@ -1,6 +1,6 @@
 <script setup lang="ts">
 import type { BucketIn } from "@/client/s3proxy";
-import { reactive, onMounted, computed, ref } from "vue";
+import { reactive, onMounted, ref } from "vue";
 import BootstrapModal from "@/components/modals/BootstrapModal.vue";
 import { useRouter } from "vue-router";
 import { Modal } from "bootstrap";
@@ -30,17 +30,13 @@ onMounted(() => {
   createBucketModal = new Modal("#" + props.modalID);
 });
 
-const formValid = computed<boolean>(
-  () => bucketCreateForm.value?.checkValidity() ?? false
-);
-
 function createBucket() {
   formState.validated = true;
   // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
   formState.bucketNameTaken = false;
   bucket.description = bucket.description.trim();
   bucket.name = bucket.name.trim();
-  if (formValid.value) {
+  if (bucketCreateForm.value?.checkValidity()) {
     formState.loading = true;
     bucketRepository.createBucket(
       bucket,
diff --git a/src/components/object-storage/modals/PermissionModal.vue b/src/components/object-storage/modals/PermissionModal.vue
index 00a4663b4832a538dfb97adeb2e61f77c711fd8d..f84ed693432b0c2eaa33f587970937ad0fcafe21 100644
--- a/src/components/object-storage/modals/PermissionModal.vue
+++ b/src/components/object-storage/modals/PermissionModal.vue
@@ -75,10 +75,6 @@ const permissionUserReadonly = computed<boolean>(() => {
   return formState.readonly || editPermission.value;
 });
 
-const formValid = computed<boolean>(
-  () => permissionForm.value?.checkValidity() ?? false
-);
-
 // Watchers
 // -----------------------------------------------------------------------------
 watch(
@@ -189,7 +185,7 @@ function findSubFolders(
  */
 function formSubmit() {
   formState.error = false;
-  if (formValid.value) {
+  if (permissionForm.value?.checkValidity()) {
     const tempPermission: BucketPermissionIn = permission;
     if (permission.from_timestamp != null) {
       tempPermission.from_timestamp =
diff --git a/src/components/parameter-schema/ParameterSchemaDescriptionComponent.vue b/src/components/parameter-schema/ParameterSchemaDescriptionComponent.vue
new file mode 100644
index 0000000000000000000000000000000000000000..43ce261108a16742579d4ca04515b9f378952f39
--- /dev/null
+++ b/src/components/parameter-schema/ParameterSchemaDescriptionComponent.vue
@@ -0,0 +1,80 @@
+<script setup lang="ts">
+import { computed } from "vue";
+import ParameterGroupDescription from "@/components/parameter-schema/description-mode/ParameterGroupDescription.vue";
+import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
+
+const props = defineProps({
+  schema: {
+    type: Object,
+    required: true,
+  },
+});
+
+type ParameterGroup = {
+  group: string;
+  title: string;
+  icon?: string;
+};
+
+const navParameterGroups = computed<ParameterGroup[]>(() =>
+  Object.keys(parameterGroups.value)
+    .map((group) => {
+      return {
+        group: group,
+        title: parameterGroups.value[group]["title"],
+        icon: parameterGroups.value[group]["fa_icon"],
+      };
+    })
+    .filter(
+      // filter all groups that have only hidden parameters
+      (group) =>
+        Object.keys(parameterGroups.value[group.group]["properties"]).filter(
+          (key) =>
+            !parameterGroups.value[group.group]["properties"][key]["hidden"]
+        ).length > 0
+    )
+);
+
+const parameterGroups = computed<Record<string, never>>(
+  () => props.schema["definitions"]
+);
+</script>
+
+<template>
+  <div class="row mb-5 align-items-start">
+    <div class="col-9">
+      <div v-for="(group, groupName) in parameterGroups" :key="groupName">
+        <parameter-group-description
+          :parameter-group="group"
+          :parameter-group-name="groupName"
+        />
+      </div>
+    </div>
+    <div
+      class="col-3 sticky-top"
+      style="top: 70px !important; max-height: calc(100vh - 150px)"
+    >
+      <nav class="h-100 bg-dark rounded-1">
+        <nav class="nav">
+          <ul class="ps-0">
+            <li
+              class="nav-link"
+              v-for="group in navParameterGroups"
+              :key="group.group"
+            >
+              <a :href="'#' + group.group"
+                ><font-awesome-icon
+                  :icon="group.icon"
+                  v-if="group.icon"
+                  class="me-2"
+                />{{ group.title }}</a
+              >
+            </li>
+          </ul>
+        </nav>
+      </nav>
+    </div>
+  </div>
+</template>
+
+<style scoped></style>
diff --git a/src/components/parameter-schema/ParameterSchemaFormComponent.vue b/src/components/parameter-schema/ParameterSchemaFormComponent.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b80fd1eb59bcedf639a898f81065ad497ae78be6
--- /dev/null
+++ b/src/components/parameter-schema/ParameterSchemaFormComponent.vue
@@ -0,0 +1,317 @@
+<script setup lang="ts">
+import { computed, ref, reactive, watch, onMounted } from "vue";
+import ParameterGroupForm from "@/components/parameter-schema/form-mode/ParameterGroupForm.vue";
+import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
+import { WorkflowExecutionService } from "@/client/workflow";
+import type { ApiError } from "@/client/workflow";
+import Ajv from "ajv";
+import type { ValidateFunction } from "ajv";
+import ParameterStringInput from "@/components/parameter-schema/form-mode/ParameterStringInput.vue";
+import { Toast } from "bootstrap";
+
+// Props
+// =============================================================================
+const props = defineProps({
+  schema: {
+    type: Object,
+    required: true,
+  },
+  workflowVersionId: {
+    type: String,
+    required: true,
+  },
+});
+
+// Bootstrap Elements
+// =============================================================================
+let errorToast: Toast | null = null;
+
+// Types
+// =============================================================================
+type ParameterGroup = {
+  group: string;
+  title: string;
+  icon?: string;
+};
+
+// JSON Schema package
+// =============================================================================
+const schemaCompiler = new Ajv({
+  strict: false,
+});
+
+let validateSchema: ValidateFunction;
+
+// Reactive State
+// =============================================================================
+const launchForm = ref<HTMLFormElement | null>(null);
+
+const formState = reactive<{
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  formInput: Record<string, any>;
+  validated: boolean;
+  pipelineNotes: string;
+  report_bucket?: string;
+  loading: boolean;
+  errorType?: string;
+}>({
+  formInput: {},
+  validated: false,
+  pipelineNotes: "",
+  report_bucket: undefined,
+  loading: false,
+  errorType: undefined,
+});
+
+// Computed Properties
+// =============================================================================
+const parameterGroups = computed<Record<string, never>>(
+  () => props.schema["definitions"]
+);
+
+// Create a list with the names of all parameter groups
+const navParameterGroups = computed<ParameterGroup[]>(() =>
+  Object.keys(parameterGroups.value)
+    .map((group) => {
+      return {
+        group: group,
+        title: parameterGroups.value[group]["title"],
+        icon: parameterGroups.value[group]["fa_icon"],
+      };
+    })
+    .filter(
+      // filter all groups that have only hidden parameters
+      (group) =>
+        Object.keys(parameterGroups.value[group.group]["properties"]).filter(
+          (key) =>
+            !parameterGroups.value[group.group]["properties"][key]["hidden"]
+        ).length > 0
+    )
+);
+
+// Watchers
+// =============================================================================
+watch(
+  () => props.schema,
+  (newValue) => {
+    updateSchema(newValue);
+  }
+);
+
+// Functions
+// =============================================================================
+/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */
+function updateSchema(schema: Record<string, any>) {
+  validateSchema = schemaCompiler.compile(props.schema);
+  const b = Object.keys(schema["definitions"]).map((groupName) => [
+    groupName,
+    Object.fromEntries(
+      Object.entries(schema["definitions"][groupName]["properties"])
+        // @ts-ignore
+        .filter(([_, parameter]) => !parameter["hidden"])
+        .map(([parameterName, parameter]) => [
+          parameterName,
+          // @ts-ignore
+          parameter["default"],
+        ])
+    ),
+  ]);
+  formState.formInput = Object.fromEntries(b);
+}
+/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/ban-ts-comment, @typescript-eslint/no-unused-vars */
+
+function startWorkflow() {
+  formState.validated = true;
+  if (launchForm.value?.checkValidity()) {
+    const realInput = Object.values(formState.formInput).reduce((acc, val) => {
+      return { ...acc, ...val };
+    });
+    const schemaValid = validateSchema(realInput);
+
+    if (!schemaValid) {
+      console.error(validateSchema.errors);
+      errorToast?.show();
+    } else {
+      formState.errorType = undefined;
+      formState.loading = true;
+      WorkflowExecutionService.workflowExecutionStartWorkflow({
+        workflow_version_id: props.workflowVersionId,
+        parameters: realInput,
+        notes: formState.pipelineNotes,
+        report_output_bucket: formState.report_bucket,
+      })
+        .then(() => {
+          console.log("Started Workflow");
+        })
+        .catch((err: ApiError) => {
+          console.error(err);
+          if (err.body["detail"].includes("workflow execution limit")) {
+            formState.errorType = "limit";
+          }
+          errorToast?.show();
+        })
+        .finally(() => {
+          formState.loading = false;
+        });
+    }
+  }
+}
+
+// Lifecycle Events
+// =============================================================================
+onMounted(() => {
+  updateSchema(props.schema);
+  errorToast = new Toast("#workflowExecutionErrorToast");
+});
+</script>
+
+<template>
+  <div class="toast-container position-fixed top-toast end-0 p-3">
+    <div
+      role="alert"
+      aria-live="assertive"
+      aria-atomic="true"
+      class="toast text-bg-danger align-items-center border-0"
+      data-bs-autohide="true"
+      id="workflowExecutionErrorToast"
+    >
+      <div class="d-flex p-2">
+        <div v-if="formState.errorType === 'limit'" class="toast-body">
+          You have too many active workflow executions to start a new one
+        </div>
+        <div v-else>
+          There was an error with starting the workflow execution. Look in the
+          console for more information
+        </div>
+        <button
+          type="button"
+          class="btn-close btn-close-white m-auto"
+          data-bs-dismiss="toast"
+          aria-label="Close"
+        ></button>
+      </div>
+    </div>
+  </div>
+  <div class="row mb-5 align-items-start">
+    <form
+      class="col-9"
+      id="launchWorkflowForm"
+      ref="launchForm"
+      :class="{ 'was-validated': formState.validated }"
+    >
+      <div class="card bg-dark mb-3">
+        <h2 class="card-header" id="pipelineGeneralOptions">
+          <font-awesome-icon icon="fa-solid fa-gear" class="me-2" />
+          Pipeline Options
+        </h2>
+        <div class="card-body">
+          <h5 class="card-title">
+            General Options about the pipeline execution
+          </h5>
+          <div class="input-group">
+            <span class="input-group-text" id="pipelineNotes">
+              <font-awesome-icon
+                class="me-2 text-dark"
+                icon="fa-solid fa-sticky-note"
+              />
+              <code>--notes</code>
+            </span>
+            <textarea
+              class="form-control"
+              rows="2"
+              v-model="formState.pipelineNotes"
+            />
+          </div>
+          <label class="mb-3"
+            >Personal notes about the pipeline execution</label
+          >
+          <div class="input-group">
+            <span class="input-group-text" id="pipelineNotes">
+              <font-awesome-icon
+                class="me-2 text-dark"
+                icon="fa-solid fa-sticky-note"
+              />
+              <code>--report_output_bucket</code>
+            </span>
+            <parameter-string-input
+              parameter-name="report_output_bucket"
+              v-model="formState.report_bucket"
+              :parameter="{
+                format: 'directory-path',
+                type: 'string',
+              }"
+            />
+          </div>
+          <label class="mb-3">
+            Directory in bucket where to save the Nextflow report about the
+            pipeline execution
+          </label>
+        </div>
+      </div>
+      <template v-for="(group, groupName) in parameterGroups" :key="groupName">
+        <parameter-group-form
+          :modelValue="formState.formInput[groupName]"
+          @update:model-value="
+            (newValue) => (formState.formInput[groupName] = newValue)
+          "
+          v-if="formState.formInput[groupName]"
+          :parameter-group-name="groupName"
+          :parameter-group="group"
+        />
+      </template>
+    </form>
+    <div
+      class="col-3 sticky-top bg-dark rounded-1 px-0"
+      style="top: 70px !important; max-height: calc(100vh - 150px)"
+    >
+      <div class="d-flex pt-2">
+        <button
+          type="submit"
+          form="launchWorkflowForm"
+          @click.prevent="startWorkflow"
+          class="btn btn-success w-50 mx-2"
+          :disabled="formState.loading"
+        >
+          <font-awesome-icon icon="fa-solid fa-rocket" class="me-2" />
+          Launch
+        </button>
+        <router-link
+          role="button"
+          class="btn btn-success w-50 mx-2"
+          target="_blank"
+          :to="{ name: 'buckets' }"
+        >
+          <font-awesome-icon icon="fa-solid fa-upload" class="me-2" />
+          Upload files
+        </router-link>
+      </div>
+      <nav class="h-100">
+        <nav class="nav">
+          <ul class="ps-0">
+            <li class="nav-link">
+              <a href="#pipelineGeneralOptions"
+                ><font-awesome-icon icon="fa-solid fa-gear" class="me-2" />
+                General Pipeline Options
+              </a>
+            </li>
+            <li
+              class="nav-link"
+              v-for="group in navParameterGroups"
+              :key="group.group"
+            >
+              <a :href="'#' + group.group"
+                ><font-awesome-icon
+                  :icon="group.icon"
+                  v-if="group.icon"
+                  class="me-2"
+                />{{ group.title }}</a
+              >
+            </li>
+          </ul>
+        </nav>
+      </nav>
+    </div>
+  </div>
+</template>
+
+<style scoped></style>
diff --git a/src/components/parameter-schema/description-mode/ParameterDescription.vue b/src/components/parameter-schema/description-mode/ParameterDescription.vue
new file mode 100644
index 0000000000000000000000000000000000000000..0c2371d0f042e31b87d79e416b5c94bad70fad70
--- /dev/null
+++ b/src/components/parameter-schema/description-mode/ParameterDescription.vue
@@ -0,0 +1,136 @@
+<script setup lang="ts">
+import { computed } from "vue";
+import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
+import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
+
+const props = defineProps({
+  parameter: {
+    type: Object,
+    required: true,
+    validator(value: Record<string, never>) {
+      return ["boolean", "array", "number", "string"].includes(value["type"]);
+    },
+  },
+  required: Boolean,
+  parameterName: {
+    type: String,
+    required: true,
+  },
+});
+
+const randomIDSuffix = Math.random().toString(16).substr(2, 8);
+
+const helpText = computed<string | undefined>(
+  () => props.parameter["help_text"]
+);
+const parameterType = computed<string>(() => props.parameter["type"]);
+const icon = computed<string | undefined>(() => props.parameter["fa_icon"]);
+const description = computed<string>(() => props.parameter["description"]);
+const defaultValue = computed<string | undefined>(() =>
+  props.parameter["default"]?.toString()
+);
+const enumValues = computed<string[] | undefined>(() =>
+  props.parameter["enum"]?.map((val: string) => val.toString())
+);
+const hidden = computed<boolean>(() => props.parameter["hidden"] ?? false);
+const parameterPattern = computed<string | undefined>(
+  () => props.parameter["pattern"]
+);
+
+const showRightColum = computed<boolean>(
+  () =>
+    helpText.value != undefined ||
+    props.required ||
+    defaultValue.value != undefined
+);
+</script>
+
+<template>
+  <div
+    class="row border-top border-bottom border-secondary align-items-start py-2"
+    v-if="!hidden"
+  >
+    <div class="fs-6 col-3">
+      <font-awesome-icon :icon="icon" v-if="icon" class="me-2" />
+      <code class="bg-dark p-1" :id="props.parameterName"
+        >--{{ props.parameterName }}</code
+      >
+      <br />
+      <span>type: '{{ parameterType }}'</span>
+    </div>
+    <div
+      :class="{ 'col-7': showRightColum, 'col-9': !showRightColum }"
+      class="flex-fill"
+    >
+      <markdown-renderer :markdown="description" />
+    </div>
+    <div
+      class="col-auto d-flex flex-column align-items-end flex-fill"
+      v-if="showRightColum"
+    >
+      <button
+        class="btn btn-outline-info btn-sm my-1"
+        type="button"
+        data-bs-toggle="collapse"
+        :data-bs-target="'#helpCollapse' + randomIDSuffix"
+        aria-expanded="false"
+        aria-controls="collapseExample"
+        v-if="helpText"
+      >
+        <font-awesome-icon icon="fa-solid fa-circle-info" />
+        Help
+      </button>
+      <div v-if="enumValues" class="dropdown w-fit my-1">
+        <a
+          class="bg-dark rounded-1 p-1 dropdown-toggle text-reset text-decoration-none"
+          href="#"
+          role="button"
+          data-bs-toggle="dropdown"
+          aria-expanded="false"
+        >
+          Options:
+          <span v-if="defaultValue"
+            ><code>{{ defaultValue }}</code> (default)</span
+          >
+        </a>
+        <ul class="dropdown-menu dropdown-menu-dark" v-if="enumValues">
+          <li v-for="val in enumValues" :key="val" class="px-2 w-100">
+            {{ val }} <span v-if="val === defaultValue">(default)</span>
+          </li>
+        </ul>
+      </div>
+      <span v-else-if="defaultValue" class="bg-dark rounded-1 py-0 px-1 my-1"
+        >default: <code>{{ defaultValue }}</code></span
+      >
+
+      <span
+        v-if="props.required"
+        class="bg-warning rounded-1 px-1 py-0 text-white"
+        >required</span
+      >
+    </div>
+    <div
+      class="collapse p-2 pb-0 bg-dark m-2 flex-shrink-1"
+      :id="'helpCollapse' + randomIDSuffix"
+      v-if="helpText"
+    >
+      <markdown-renderer class="helpTextCode" :markdown="helpText" />
+      <span v-if="parameterPattern" class="mb-2"
+        >Pattern: <code>{{ parameterPattern }}</code></span
+      >
+    </div>
+  </div>
+</template>
+
+<style scoped>
+code {
+  color: var(--bs-code-color) !important;
+}
+
+li:hover {
+  background: var(--bs-secondary);
+}
+a:hover {
+  filter: brightness(0.8);
+}
+</style>
diff --git a/src/components/parameter-schema/description-mode/ParameterGroupDescription.vue b/src/components/parameter-schema/description-mode/ParameterGroupDescription.vue
new file mode 100644
index 0000000000000000000000000000000000000000..f8cc45c630c199a6ac292be346af74cfc8c4bdb9
--- /dev/null
+++ b/src/components/parameter-schema/description-mode/ParameterGroupDescription.vue
@@ -0,0 +1,58 @@
+<script setup lang="ts">
+import { computed } from "vue";
+import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
+import ParameterDescription from "@/components/parameter-schema/description-mode/ParameterDescription.vue";
+
+const props = defineProps({
+  parameterGroup: {
+    type: Object,
+    required: true,
+    validator(value: Record<string, never>) {
+      return "object" === value["type"];
+    },
+  },
+  parameterGroupName: {
+    type: String,
+    required: true,
+  },
+});
+
+const title = computed<string>(() => props.parameterGroup["title"]);
+const icon = computed<string>(() => props.parameterGroup["fa_icon"]);
+const description = computed<string>(() => props.parameterGroup["description"]);
+const groupHidden = computed<boolean>(() =>
+  Object.keys(parameters.value).reduce(
+    (acc: boolean, val: string) => acc && parameters.value[val]["hidden"],
+    true
+  )
+);
+const parameters = computed<Record<string, never>>(
+  () => props.parameterGroup["properties"]
+);
+</script>
+
+<template>
+  <div class="mb-5" v-if="!groupHidden">
+    <div class="row">
+      <h2 :id="props.parameterGroupName">
+        <font-awesome-icon :icon="icon" class="me-3" v-if="icon" />{{ title }}
+      </h2>
+      <h4>{{ description }}</h4>
+    </div>
+    <template
+      v-for="(parameter, parameterName) in parameters"
+      :key="parameterName"
+    >
+      <parameter-description
+        v-if="parameter['type'] !== 'object'"
+        :parameter-name="parameterName"
+        :parameter="parameter"
+        :required="
+          props.parameterGroup['required']?.includes(parameterName) ?? false
+        "
+      />
+    </template>
+  </div>
+</template>
+
+<style scoped></style>
diff --git a/src/components/parameter-schema/form-mode/ParameterBooleanInput.vue b/src/components/parameter-schema/form-mode/ParameterBooleanInput.vue
new file mode 100644
index 0000000000000000000000000000000000000000..2ee50311314e471790e694dd2574ce0b895de99e
--- /dev/null
+++ b/src/components/parameter-schema/form-mode/ParameterBooleanInput.vue
@@ -0,0 +1,73 @@
+<script setup lang="ts">
+import { computed } from "vue";
+
+const props = defineProps({
+  parameter: {
+    type: Object,
+    required: true,
+    validator(value: Record<string, never>) {
+      return "number" === value["type"];
+    },
+  },
+  required: Boolean,
+  parameterName: {
+    type: String,
+    required: true,
+  },
+  modelValue: {
+    type: Boolean,
+  },
+  helpId: {
+    type: String,
+  },
+});
+
+const randomIDSuffix = Math.random().toString(16).substr(2, 8);
+
+const helpTextPresent = computed<boolean>(() => props.parameter["help_text"]);
+const defaultValue = computed<boolean>(
+  () => props.parameter["default"] ?? false
+);
+
+const emit = defineEmits<{
+  (e: "update:modelValue", value: boolean): void;
+}>();
+</script>
+
+<template>
+  <div
+    class="flex-fill mb-0 text-bg-light fs-6 ps-4 d-flex align-items-center justify-content-start"
+    :class="{ 'rounded-end': !helpTextPresent }"
+  >
+    <div class="form-check form-check-inline">
+      <label class="form-check-label" :for="'trueOption' + randomIDSuffix"
+        >True</label
+      >
+      <input
+        class="form-check-input"
+        type="radio"
+        :name="'inlineRadioOptions' + randomIDSuffix"
+        :id="'trueOption' + randomIDSuffix"
+        :value="true"
+        :checked="defaultValue"
+        @input="emit('update:modelValue', true)"
+      />
+    </div>
+    <div class="form-check form-check-inline">
+      <input
+        class="form-check-input"
+        type="radio"
+        :name="'inlineRadioOptions' + randomIDSuffix"
+        :id="'falseOption' + randomIDSuffix"
+        :value="false"
+        @input="emit('update:modelValue', false)"
+        :checked="!defaultValue"
+      />
+      <label class="form-check-label" :for="'falseOption' + randomIDSuffix"
+        >False</label
+      >
+    </div>
+  </div>
+</template>
+
+<style scoped></style>
diff --git a/src/components/parameter-schema/form-mode/ParameterEnumInput.vue b/src/components/parameter-schema/form-mode/ParameterEnumInput.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a2e20d7c16c872493b4b16e126bacd2889afab3c
--- /dev/null
+++ b/src/components/parameter-schema/form-mode/ParameterEnumInput.vue
@@ -0,0 +1,59 @@
+<script setup lang="ts">
+import { computed, ref } from "vue";
+
+const props = defineProps({
+  parameter: {
+    type: Object,
+    required: true,
+    validator(value: Record<string, never>) {
+      return "string" === value["type"] && value["enum"];
+    },
+  },
+  required: Boolean,
+  parameterName: {
+    type: String,
+    required: true,
+  },
+  modelValue: {
+    type: String,
+  },
+  helpId: {
+    type: String,
+  },
+});
+
+const defaultValue = computed<string>(() => props.parameter["default"]);
+
+const possibleValues = computed<string[]>(() => props.parameter["enum"]);
+
+const enumSelection = ref<HTMLSelectElement | undefined>(undefined);
+
+const emit = defineEmits<{
+  (e: "update:modelValue", value: string | undefined): void;
+}>();
+
+function updateValue() {
+  emit("update:modelValue", enumSelection.value?.value);
+}
+</script>
+
+<template>
+  <select
+    ref="enumSelection"
+    :value="props.modelValue"
+    @input="updateValue"
+    class="form-select"
+    :required="required"
+    :aria-describedby="props.helpId"
+  >
+    <option
+      v-for="val in possibleValues"
+      :key="val"
+      :selected="defaultValue === val"
+    >
+      {{ val }}
+    </option>
+  </select>
+</template>
+
+<style scoped></style>
diff --git a/src/components/parameter-schema/form-mode/ParameterGroupForm.vue b/src/components/parameter-schema/form-mode/ParameterGroupForm.vue
new file mode 100644
index 0000000000000000000000000000000000000000..c68eb662a236b5fbaf1583530267492f189cb7ed
--- /dev/null
+++ b/src/components/parameter-schema/form-mode/ParameterGroupForm.vue
@@ -0,0 +1,185 @@
+<script setup lang="ts">
+import FontAwesomeIcon from "@/components/FontAwesomeIcon.vue";
+import { computed, watch } from "vue";
+import ParameterNumberInput from "@/components/parameter-schema/form-mode/ParameterNumberInput.vue";
+import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
+import ParameterBooleanInput from "@/components/parameter-schema/form-mode/ParameterBooleanInput.vue";
+import ParameterEnumInput from "@/components/parameter-schema/form-mode/ParameterEnumInput.vue";
+import ParameterStringInput from "@/components/parameter-schema/form-mode/ParameterStringInput.vue";
+
+const props = defineProps({
+  parameterGroup: {
+    type: Object,
+    required: true,
+    validator(value: Record<string, never>) {
+      return "object" === value["type"];
+    },
+  },
+  parameterGroupName: {
+    type: String,
+    required: true,
+  },
+  modelValue: {
+    type: Object,
+    required: true,
+  },
+});
+const title = computed<string>(() => props.parameterGroup["title"]);
+const icon = computed<string>(() => props.parameterGroup["fa_icon"]);
+const description = computed<string>(() => props.parameterGroup["description"]);
+const groupHidden = computed<boolean>(() =>
+  Object.keys(parameters.value).reduce(
+    (acc: boolean, val: string) => acc && parameters.value[val]["hidden"],
+    true
+  )
+);
+const parameters = computed<Record<string, never>>(
+  () => props.parameterGroup["properties"]
+);
+
+const formInput = computed(() => props.modelValue);
+const emit = defineEmits<{
+  (
+    e: "update:modelValue",
+    value: Record<string, number | string | boolean | undefined>
+  ): void;
+}>();
+
+function parameterRequired(
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any
+  parameterGroup: Record<string, any>,
+  parameterName: string
+): boolean {
+  return (
+    parameterGroup["required"]?.includes(parameterName) || // parameter is required
+    parameterGroup["dependentRequired"]?.[parameterName] // parameter is required when another parameter is set
+      ?.map((param: string) => formInput.value[param])
+      ?.reduce((acc: boolean, val: string) => acc || val, false)
+  );
+}
+
+watch(
+  formInput,
+  (newVal) => {
+    //console.log("Group", props.parameterGroupName, newVal);
+    emit("update:modelValue", newVal);
+  },
+  {
+    deep: true,
+  }
+);
+</script>
+
+<template>
+  <div class="card bg-dark mb-3" v-if="!groupHidden">
+    <h2 class="card-header" :id="props.parameterGroupName">
+      <font-awesome-icon :icon="icon" class="me-2" v-if="icon" />
+      {{ title }}
+    </h2>
+    <div class="card-body">
+      <h5 class="card-title" v-if="description">{{ description }}</h5>
+      <template
+        v-for="(parameter, parameterName) in parameters"
+        :key="parameterName"
+      >
+        <template v-if="!parameter['hidden']">
+          <div class="input-group">
+            <span class="input-group-text" :id="parameterName + '-help'">
+              <font-awesome-icon
+                class="me-2 text-dark"
+                :icon="parameter['fa_icon']"
+                v-if="parameter['fa_icon']"
+              />
+              <code>--{{ parameterName }}</code>
+            </span>
+            <parameter-number-input
+              v-if="
+                parameter['type'] === 'number' ||
+                parameter['type'] === 'integer'
+              "
+              :parameter-name="parameterName"
+              :parameter="parameter"
+              :help-id="parameterName + '-help'"
+              :required="parameterRequired(parameterGroup, parameterName)"
+              :model-value="formInput[parameterName]"
+              @update:model-value="
+                (newValue) => (formInput[parameterName] = newValue)
+              "
+            />
+            <parameter-boolean-input
+              v-else-if="parameter['type'] === 'boolean'"
+              :parameter-name="parameterName"
+              :parameter="parameter"
+              :help-id="parameterName + '-help'"
+              :model-value="formInput[parameterName]"
+              @update:model-value="
+                (newValue) => (formInput[parameterName] = newValue)
+              "
+            />
+            <template v-else-if="parameter['type'] === 'string'">
+              <parameter-enum-input
+                v-if="parameter['enum']"
+                :parameter-name="parameterName"
+                :parameter="parameter"
+                :model-value="formInput[parameterName]"
+                :required="parameterRequired(parameterGroup, parameterName)"
+                @update:model-value="
+                  (newValue) => (formInput[parameterName] = newValue)
+                "
+              />
+              <parameter-string-input
+                v-else
+                :parameter-name="parameterName"
+                :parameter="parameter"
+                :model-value="formInput[parameterName]"
+                :required="parameterRequired(parameterGroup, parameterName)"
+                @update:model-value="
+                  (newValue) => (formInput[parameterName] = newValue)
+                "
+              />
+            </template>
+            <span
+              class="input-group-text cursor-pointer px-2"
+              v-if="parameter['help_text']"
+              data-bs-toggle="collapse"
+              :data-bs-target="'#helpCollapse' + parameterName"
+              aria-expanded="false"
+              aria-controls="collapseExample"
+            >
+              <font-awesome-icon
+                class="cursor-pointer"
+                icon="fa-solid fa-circle-question"
+              />
+            </span>
+          </div>
+          <label v-if="parameter['description']"
+            ><markdown-renderer :markdown="parameter['description']"
+          /></label>
+          <div
+            class="collapse p-2 pb-0 bg-dark mx-2 mt-1 flex-shrink-1"
+            :id="'helpCollapse' + parameterName"
+            v-if="parameter['help_text']"
+          >
+            <markdown-renderer
+              class="helpTextCode"
+              :markdown="parameter['help_text']"
+            />
+            <p v-if="parameter['pattern']">
+              Pattern: <code>{{ parameter["pattern"] }}</code>
+            </p>
+          </div>
+        </template>
+      </template>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+div.card-body {
+  filter: brightness(0.9);
+}
+span.cursor-pointer:hover {
+  color: var(--bs-info);
+  background: var(--bs-light);
+}
+</style>
diff --git a/src/components/parameter-schema/form-mode/ParameterNumberInput.vue b/src/components/parameter-schema/form-mode/ParameterNumberInput.vue
new file mode 100644
index 0000000000000000000000000000000000000000..513b783282e6b08ce8e78b4dd4360ddd4fe963ce
--- /dev/null
+++ b/src/components/parameter-schema/form-mode/ParameterNumberInput.vue
@@ -0,0 +1,50 @@
+<script setup lang="ts">
+import { ref } from "vue";
+const props = defineProps({
+  parameter: {
+    type: Object,
+    required: true,
+    validator(value: Record<string, never>) {
+      return "number" === value["type"] || "integer" === value["type"];
+    },
+  },
+  required: Boolean,
+  parameterName: {
+    type: String,
+    required: true,
+  },
+  modelValue: {
+    type: Number,
+  },
+  helpId: {
+    type: String,
+  },
+});
+
+const emit = defineEmits<{
+  (e: "update:modelValue", value: number | undefined): void;
+}>();
+
+const numberInput = ref<HTMLInputElement | undefined>(undefined);
+
+function updateValue() {
+  emit("update:modelValue", Number(numberInput.value?.value));
+}
+</script>
+
+<template>
+  <input
+    class="form-control"
+    type="number"
+    ref="numberInput"
+    :max="props.parameter['maximum']"
+    :min="props.parameter['minimum']"
+    step="0.01"
+    :value="props.modelValue"
+    :required="props.required"
+    :aria-describedby="props.helpId"
+    @input="updateValue"
+  />
+</template>
+
+<style scoped></style>
diff --git a/src/components/parameter-schema/form-mode/ParameterStringInput.vue b/src/components/parameter-schema/form-mode/ParameterStringInput.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b6be2cb88238b1e10ead54d2a32c37cd99ac0505
--- /dev/null
+++ b/src/components/parameter-schema/form-mode/ParameterStringInput.vue
@@ -0,0 +1,189 @@
+<script setup lang="ts">
+import { computed, watch, ref, onMounted, reactive } from "vue";
+import { useBucketStore } from "@/stores/buckets";
+import { ObjectService } from "@/client/s3proxy";
+
+const bucketRepository = useBucketStore();
+
+const props = defineProps({
+  parameter: {
+    type: Object,
+    required: true,
+    validator(value: Record<string, never>) {
+      return "string" === value["type"] && value["enum"];
+    },
+  },
+  required: Boolean,
+  parameterName: {
+    type: String,
+    required: true,
+  },
+  modelValue: {
+    type: String,
+  },
+  helpId: {
+    type: String,
+  },
+});
+
+const randomIDSuffix = Math.random().toString(16).substr(2, 8);
+
+const defaultValue = computed<string>(() => props.parameter["default"]);
+
+const s3Path = reactive<{
+  bucket: string | undefined;
+  key: string | undefined;
+}>({
+  bucket: undefined,
+  key: undefined,
+});
+
+const keysInBucket = ref<string[]>([]);
+
+watch(defaultValue, (newVal, oldVal) => {
+  if (newVal != oldVal && newVal != undefined) {
+    emit("update:modelValue", newVal);
+  }
+});
+
+watch(s3Path, () => {
+  if (format.value) {
+    updateValue();
+  }
+});
+
+watch(
+  () => s3Path.bucket,
+  (newVal, oldVal) => {
+    if (newVal !== oldVal) {
+      updateKeysInBucket(newVal);
+    }
+  }
+);
+
+const pattern = computed<string>(() => props.parameter["pattern"]);
+
+const emit = defineEmits<{
+  (e: "update:modelValue", value: string | undefined): void;
+}>();
+
+const stringInput = ref<HTMLInputElement | undefined>(undefined);
+
+const format = computed<string | undefined>(() => props.parameter["format"]);
+
+const filesInBucket = computed<string[]>(() =>
+  keysInBucket.value.filter(
+    (obj) => !obj.endsWith("/") && !obj.endsWith(".s3keep")
+  )
+);
+
+const foldersInBucket = computed<string[]>(() =>
+  keysInBucket.value
+    .map((obj) => {
+      const parts = obj.split("/");
+      return parts
+        .slice(0, parts.length - 1)
+        .map((part, index) =>
+          parts.slice(0, index + 1).reduce((acc, val) => `${acc}/${val}`)
+        );
+    })
+    .flat()
+    .filter((val, index, array) => array.indexOf(val) === index)
+);
+
+const filesAndFoldersInBucket = computed<string[]>(() =>
+  filesInBucket.value.concat(foldersInBucket.value)
+);
+
+const keyDataList = computed<string[]>(() => {
+  switch (format.value) {
+    case "file-path":
+      return filesInBucket.value;
+    case "directory-path":
+      return foldersInBucket.value;
+    case "path":
+      return filesAndFoldersInBucket.value;
+    default:
+      return [];
+  }
+});
+
+function updateValue() {
+  if (format.value) {
+    emit(
+      "update:modelValue",
+      !s3Path.bucket && s3Path.key
+        ? undefined
+        : `s3://${s3Path.bucket}/${s3Path.key}`
+    );
+  } else {
+    emit(
+      "update:modelValue",
+      stringInput.value?.value ? stringInput.value?.value : undefined
+    );
+  }
+}
+
+const helpTextPresent = computed<boolean>(() => props.parameter["help_text"]);
+
+function updateKeysInBucket(bucketName?: string) {
+  if (bucketName != null) {
+    ObjectService.objectGetBucketObjects(bucketName).then((objs) => {
+      keysInBucket.value = objs.map((obj) => obj.key);
+    });
+  } else {
+    keysInBucket.value = [];
+  }
+}
+
+onMounted(() => {
+  bucketRepository.fetchBuckets();
+  if (format.value) {
+    s3Path.key = defaultValue.value;
+  }
+});
+</script>
+
+<template>
+  <template v-if="format">
+    <select
+      class="form-select"
+      :required="props.required"
+      v-model="s3Path.bucket"
+    >
+      <option selected disabled value="">Please select a bucket</option>
+      <option
+        v-for="bucket in bucketRepository.ownBucketsAndFullPermissions"
+        :key="bucket"
+        :value="bucket"
+      >
+        {{ bucket }}
+      </option>
+    </select>
+    <input
+      class="form-control"
+      :class="{ 'rounded-end': !helpTextPresent }"
+      :list="'datalistOptions2' + randomIDSuffix"
+      placeholder="Type to search in bucket..."
+      :required="props.required && format === 'file-path'"
+      v-model="s3Path.key"
+      :pattern="pattern"
+    />
+    <datalist :id="'datalistOptions2' + randomIDSuffix">
+      <option v-for="obj in keyDataList" :value="obj" :key="obj" />
+    </datalist>
+  </template>
+  <input
+    v-else
+    ref="stringInput"
+    class="form-control"
+    type="text"
+    :value="props.modelValue"
+    :required="props.required"
+    :aria-describedby="props.helpId"
+    :pattern="pattern"
+    @input="updateValue"
+  />
+</template>
+
+<style scoped></style>
diff --git a/src/components/workflows/WorkflowCard.vue b/src/components/workflows/WorkflowCard.vue
index 1c2ac3a8cce1d74ad09c8ec68a25f5bf698d5e92..ffddccfe35a024c27b092626aa737f0f36a763a8 100644
--- a/src/components/workflows/WorkflowCard.vue
+++ b/src/components/workflows/WorkflowCard.vue
@@ -50,7 +50,6 @@ onMounted(() => {
           v-if="latestVersion?.icon_url != null"
           :src="latestVersion.icon_url"
           class="img-fluid float-end icon"
-          alt="Workflow icon"
         />
       </div>
       <p class="card-text" :class="{ 'text-truncate': truncateDescription }">
diff --git a/src/stores/buckets.ts b/src/stores/buckets.ts
index e3ab60b32801feb038b904a8293c0ec9cf6502e9..c95040f8eeee280d35e822af6ed3fdb2f55cb4af 100644
--- a/src/stores/buckets.ts
+++ b/src/stores/buckets.ts
@@ -3,6 +3,7 @@ import {
   BucketPermissionService,
   BucketService,
   Constraint,
+  Permission,
 } from "@/client/s3proxy";
 import type {
   BucketOut,
@@ -10,6 +11,7 @@ import type {
   BucketPermissionOut,
 } from "@/client/s3proxy";
 import { useAuthStore } from "@/stores/auth";
+import type { CancelablePromise } from "@/client/auth";
 
 export const useBucketStore = defineStore({
   id: "buckets",
@@ -17,11 +19,27 @@ export const useBucketStore = defineStore({
     ({
       buckets: [],
       ownPermissions: {},
+      _lastFetchBucketPromise: undefined,
+      _bla: 0,
     } as {
       buckets: BucketOut[];
       ownPermissions: Record<string, BucketPermissionOut>;
+      _lastFetchBucketPromise?: CancelablePromise<never>;
+      _bla: number;
     }),
   getters: {
+    ownBucketsAndFullPermissions(): string[] {
+      const names = this.buckets
+        .map((bucket) => bucket.name)
+        .concat(
+          Object.values(this.ownPermissions)
+            .filter((perm) => perm.permission === Permission.READWRITE)
+            .map((perm) => perm.bucket_name)
+        );
+      names.sort();
+      return names;
+    },
+
     permissionFeatureAllowed(): (bucketName: string) => boolean {
       return (bucketName) => {
         // If a permission for the bucket exist, then false
@@ -131,15 +149,27 @@ export const useBucketStore = defineStore({
       onRejected: ((reason: any) => void) | null | undefined = null,
       onFinally: (() => void) | null | undefined = null
     ) {
-      const authStore = useAuthStore();
-      BucketService.bucketListBuckets(authStore.currentUID)
-        .then((buckets) => {
-          this.buckets = buckets;
-          onFulfilled?.(buckets);
-        })
-        .catch(onRejected)
-        .finally(onFinally);
-      this._fetchOwnPermissions();
+      /* If the time between two calls to this function is less than 5 seconds,
+       * then no API call will be made and the last one reused
+       */
+      const currentTime = new Date().getTime();
+      if (currentTime - this._bla > 5000) {
+        this._bla = currentTime;
+        const authStore = useAuthStore();
+        BucketService.bucketListBuckets(authStore.currentUID)
+          .then((buckets) => {
+            this.buckets = buckets;
+            onFulfilled?.(buckets);
+          })
+          .catch(onRejected)
+          .finally(onFinally);
+        this._fetchOwnPermissions();
+      } else {
+        this._lastFetchBucketPromise
+          ?.then(onFulfilled)
+          .catch(onRejected)
+          .finally(onFinally);
+      }
     },
     fetchBucket(
       bucketName: string,
diff --git a/src/views/workflows/WorkflowVersionView.vue b/src/views/workflows/WorkflowVersionView.vue
index 9ffb18d2f806e318300a3410824feb094ac3026a..c450386da0df32cdc98e00c1c072ae6fcac951b6 100644
--- a/src/views/workflows/WorkflowVersionView.vue
+++ b/src/views/workflows/WorkflowVersionView.vue
@@ -4,6 +4,7 @@ import { WorkflowVersionService } from "@/client/workflow";
 import type { WorkflowVersionFull } from "@/client/workflow";
 import axios from "axios";
 import MarkdownRenderer from "@/components/MarkdownRenderer.vue";
+import ParameterSchemaDescriptionComponent from "@/components/parameter-schema/ParameterSchemaDescriptionComponent.vue";
 
 const props = defineProps<{
   versionId: string;
@@ -127,13 +128,14 @@ onMounted(() => {
       ></span>
     </p>
   </div>
-  <div v-else>
+  <div v-else class="px-2">
     <p v-if="props.activeTab === 'description'">
       <markdown-renderer :markdown="versionState.descriptionMarkdown" />
     </p>
-    <pre v-else-if="props.activeTab === 'parameters'"
-      >{{ JSON.stringify(versionState.parameterSchema, null, 2) }}
-    </pre>
+    <parameter-schema-description-component
+      v-else-if="props.activeTab === 'parameters'"
+      :schema="versionState.parameterSchema"
+    />
     <p v-else-if="props.activeTab === 'changes'">
       <markdown-renderer :markdown="versionState.changelogMarkdown" />
     </p>
diff --git a/src/views/workflows/WorkflowView.vue b/src/views/workflows/WorkflowView.vue
index f8fd224a7a1d91f5aa3b06ae980460f9124acd1c..9a5b54fc2778d4531c8f134cfc42095633b0e40d 100644
--- a/src/views/workflows/WorkflowView.vue
+++ b/src/views/workflows/WorkflowView.vue
@@ -169,6 +169,7 @@ onMounted(() => {
           params: {
             versionId: latestVersion.git_commit_hash,
           },
+          query: { tab: route.query.tab },
         }"
         >Try the latest version {{ latestVersion?.version }}.</router-link
       >