diff --git a/src/api/worker.ts b/src/api/worker.ts
index 0f8fc10d218cca4bb051ef252ca2b06cfe2c649d..6466290b6a115953ad49f17f194ae07a27bde9b4 100644
--- a/src/api/worker.ts
+++ b/src/api/worker.ts
@@ -13,6 +13,8 @@ export interface ListWorkersParameters extends PageNumberPaginationParameters {
 
 export interface CreateWorkerPayload extends Pick<Worker, 'name' | 'slug'> { type: WorkerType['slug'] }
 
+export interface CreateWorkerVersionPayload extends Pick<WorkerVersion, 'configuration'>, Partial<Pick<WorkerVersion, 'docker_image' | 'docker_image_iid' | 'state' | 'gpu_usage' | 'model_usage'>> { revision_id?: UUID }
+
 // List executable workers
 export const listWorkers = unique(async (params: ListWorkersParameters): Promise<PageNumberPagination<Worker>> => (await axios.get('/workers/', { params })).data)
 
@@ -37,7 +39,10 @@ export const retrieveWorker = unique(async (id: UUID): Promise<Worker> => (await
  * This call will throw a DuplicatedWorker error in case a worker already exists with this slug and is executable by the user.
  */
 export const createWorker = unique(async (params: CreateWorkerPayload): Promise<Worker> => {
-  const result = await axios.post(`/workers/`, params)
+  const result = await axios.post('/workers/', params)
   if (result.status === 200) throw new DuplicatedWorker()
   return result.data
 })
+
+// Create a worker version
+export const createWorkerVersion = unique(async (workerId: UUID, params: CreateWorkerVersionPayload): Promise<WorkerVersion> => (await axios.post(`/workers/${workerId}/versions/`, params)).data)
diff --git a/src/components/Process/Workers/Versions/CreateForm.vue b/src/components/Process/Workers/Versions/CreateForm.vue
new file mode 100644
index 0000000000000000000000000000000000000000..432f4de0e87758aedc659c42ee994b9ccca06963
--- /dev/null
+++ b/src/components/Process/Workers/Versions/CreateForm.vue
@@ -0,0 +1,240 @@
+<template>
+  <button
+    class="button is-primary"
+    v-on:click="openModal = allowCreation"
+    v-bind="$attrs"
+    :disabled="!allowCreation || null"
+    :title="allowTitle"
+  >
+    Create
+  </button>
+
+  <Modal v-model="openModal" :title="modalTitle" is-large>
+    <form class="form" v-on:submit.prevent="create">
+      <div class="field">
+        <label class="label">Docker image reference</label>
+        <div class="control">
+          <input
+            class="input"
+            :class="{ 'is-danger': fieldErrors.docker_image_iid }"
+            type="text"
+            placeholder="…"
+            maxlength="512"
+            v-model.trim="payload.docker_image_iid"
+            :disabled="loading || null"
+          />
+        </div>
+        <p v-if="fieldErrors.docker_image_iid" class="help is-danger">{{ fieldErrors.docker_image_iid.join(', ') }}</p>
+      </div>
+
+      <div class="field">
+        <label class="label">
+          JSON configuration
+          <div class="dropdown is-hoverable">
+            <div class="dropdown-trigger">
+              <i class="icon-help has-text-info"></i>
+            </div>
+            <div class="dropdown-menu">
+              <div class="message message-body help-message-width">
+                <p>You can define here any JSON configuration for this version.</p>
+                <p>
+                  If you used <a href="https://pypi.org/project/arkindex-base-worker/" target="_blank">arkindex-base-worker</a>
+                  to create your image, this value will be accessible as <samp class="tag is-light">self.config</samp> within the worker class.</p>
+                <p>Please refer to the <a href="https://doc.arkindex.org" target="_blank">documentation</a> for more information.</p>
+              </div>
+            </div>
+          </div>
+        </label>
+        <div class="control">
+          <textarea
+            class="textarea is-family-monospace"
+            :class="{ 'is-danger': Boolean(JSONError || fieldErrors.configuration) }"
+            v-model="configurationString"
+            :disabled="loading || null"
+          ></textarea>
+        </div>
+        <p v-if="JSONError" class="help is-danger">{{ JSONError }}</p>
+        <p v-if="fieldErrors.configuration" class="help is-danger">{{ fieldErrors.configuration.join(', ') }}</p>
+      </div>
+
+      <div class="field">
+        <div class="control">
+          <input
+            id="switchModel"
+            type="checkbox"
+            class="switch is-rtl is-rounded is-info"
+            v-model="payload.model_usage"
+            :disabled="loading || null"
+          />
+          <label for="switchModel">Use a model</label>
+        </div>
+        <p v-if="fieldErrors.model_usage" class="help is-danger">{{ fieldErrors.model_usage.join(', ') }}</p>
+      </div>
+
+      <div class="field">
+        <label class="label">GPU usage</label>
+        <div class="control">
+          <div class="select" :class="{ 'is-danger': fieldErrors.gpu_usage }">
+            <select
+              v-model="payload.gpu_usage"
+              :disabled="loading || null"
+            >
+              <option
+                v-for="(value, key) in WorkerVersionGPUUsage"
+                :key="key"
+                :value="value"
+                :title="key"
+              >
+                {{ key }}
+              </option>
+            </select>
+          </div>
+        </div>
+        <p v-if="fieldErrors.gpu_usage" class="help is-danger">{{ fieldErrors.gpu_usage.join(', ') }}</p>
+      </div>
+    </form>
+
+    <template v-slot:footer="{ close }">
+      <button class="button" v-on:click="close">Cancel</button>
+      <button
+        type="submit"
+        v-on:click="create"
+        class="button is-primary ml-auto"
+        :class="{ 'is-loading': loading }"
+        :disabled="JSONError || null"
+        :title="saveTitle"
+      >
+        Create
+      </button>
+    </template>
+  </Modal>
+</template>
+
+<script lang="ts">
+import { mapState, mapActions } from 'pinia'
+import { errorParser, ensureArray } from '@/helpers'
+import Modal from '@/components/Modal.vue'
+import { useNotificationStore, useWorkerStore } from '@/stores'
+import { CreateWorkerVersionPayload } from '@/api'
+import { defineComponent } from 'vue'
+import { isAxiosError } from 'axios'
+import { Worker, isWorker } from '@/types/worker'
+import { WorkerVersionGPUUsage, WorkerVersionState } from '@/enums'
+
+export default defineComponent({
+  emits: ['created'],
+  components: {
+    Modal
+  },
+  props: {
+    workerId: {
+      type: String,
+      required: true
+    }
+  },
+  data: () => ({
+    WorkerVersionGPUUsage,
+    openModal: false,
+    configurationString: '{}',
+    JSONError: null as string | null,
+    payload: {
+      docker_image_iid: '',
+      configuration: {},
+      model_usage: false,
+      gpu_usage: WorkerVersionGPUUsage.Disabled
+    } as CreateWorkerVersionPayload,
+    loading: false,
+    fieldErrors: {} as Partial<Record<keyof CreateWorkerVersionPayload, string[]>>
+  }),
+  computed: {
+    ...mapState(useWorkerStore, ['workers']),
+    worker (): Worker | null {
+      const worker = this.workers?.[this.workerId]
+      if (!worker || !isWorker(worker)) return null
+      return worker
+    },
+    allowCreation () {
+      if (this.worker === null) return false
+      return this.worker.repository_id === null
+    },
+    allowTitle () {
+      if (this.worker === null) return ''
+      return this.worker.repository_id === null
+        ? 'Create a version'
+        : 'Versions cannot be created on workers linked to a repository'
+    },
+    modalTitle () {
+      return this.worker ? `Create a version for worker "${this.worker.name}"` : 'Create a version'
+    },
+    saveTitle () {
+      return this.JSONError ? 'Please enter a valid configuration' : 'Create a new version'
+    }
+  },
+  methods: {
+    ...mapActions(useWorkerStore, ['createWorkerVersion', 'getWorker']),
+    ...mapActions(useNotificationStore, ['notify']),
+    async create () {
+      if (this.JSONError) return
+      this.loading = true
+      this.fieldErrors = {}
+      try {
+        const cleanPayload = { ...this.payload }
+        if (cleanPayload.docker_image_iid) {
+          // Automatically mark the worker as available when a docker image is set
+          cleanPayload.state = WorkerVersionState.Available
+        } else {
+          delete cleanPayload.docker_image_iid
+        }
+        await this.createWorkerVersion(this.workerId, cleanPayload)
+        this.notify({ type: 'info', text: 'Worker version created successfully.' })
+        this.openModal = false
+        this.payload = {
+          docker_image_iid: '',
+          configuration: {},
+          model_usage: false,
+          gpu_usage: WorkerVersionGPUUsage.Disabled
+        }
+        this.configurationString = '{}'
+        this.$emit('created')
+      } catch (err) {
+        this.notify({ type: 'error', text: errorParser(err) })
+        if (isAxiosError(err)) {
+          this.fieldErrors = err?.response?.data
+            ? Object.fromEntries(Object.entries(err?.response?.data).map(([k, v]) => [k, ensureArray(v)]))
+            : {}
+        }
+      } finally {
+        this.loading = false
+      }
+    }
+  },
+  watch: {
+    worker () {
+      // Fetch the worker individually to know if it is linked to a repository
+      const worker = this.workers?.[this.workerId]
+      if (worker && !isWorker(worker)) this.getWorker(this.workerId)
+    },
+    configurationString (newValue) {
+      this.JSONError = null
+      try {
+        const data = JSON.parse(newValue)
+        // Update the payload value directly when checking errors
+        if (typeof data === 'string' || Array.isArray(data)) {
+          this.JSONError = 'Configuration JSON body must be an object'
+        } else {
+          this.payload.configuration = data
+        }
+      } catch (e) {
+        if (e instanceof SyntaxError) this.JSONError = e.message
+        else throw e
+      }
+    }
+  }
+})
+</script>
+
+<style scoped>
+.help-message-width {
+  width: min(50vw, 100rem);
+}
+</style>
diff --git a/src/components/Process/Workers/Versions/List.vue b/src/components/Process/Workers/Versions/List.vue
index 7c4f0bc5a27fe20e67c22510fe5f83bec1404d76..4a0cbb631b3701a99efae5eb545af850a64efe97 100644
--- a/src/components/Process/Workers/Versions/List.vue
+++ b/src/components/Process/Workers/Versions/List.vue
@@ -6,8 +6,9 @@
       class="switch is-rtl is-rounded is-info"
       v-model="advancedMode"
     />
-    <label class="is-pulled-right" for="switchAll">Display all versions</label>
+    <label class="is-pulled-right ml-3" for="switchAll">Display all versions</label>
   </template>
+  <CreateForm :worker-id="worker.id" class="is-pulled-right" v-on:created="fetchVersions" />
   <h2 class="title is-4">Versions</h2>
   <span class="is-clearfix"></span>
   <div v-if="versionsError" class="notification is-warning">{{ versionsError }}</div>
@@ -64,9 +65,11 @@ import { useNotificationStore, useWorkerStore } from '@/stores'
 import { WorkerVersionListParameters } from '@/api'
 import { PageNumberPagination } from '@/types'
 import { WorkerVersion, Worker } from '@/types/worker'
+import CreateForm from '@/components/Process/Workers/Versions/CreateForm.vue'
 
 export default defineComponent({
   components: {
+    CreateForm,
     Paginator,
     Row
   },
diff --git a/src/stores/workers.ts b/src/stores/workers.ts
index abadce8581f3883b685400444d16bcbeb38a418b..8791303c53bbc6f23168768add056e0d3397bb34 100644
--- a/src/stores/workers.ts
+++ b/src/stores/workers.ts
@@ -21,6 +21,8 @@ import {
   retrieveWorkerRun,
   CreateWorkerPayload,
   createWorker,
+  CreateWorkerVersionPayload,
+  createWorkerVersion,
   listUserWorkerRuns
 } from '@/api'
 import { isAxiosError } from 'axios'
@@ -159,6 +161,12 @@ export const useWorkerStore = defineStore('worker', {
       return resp
     },
 
+    async createWorkerVersion (workerId: UUID, params: CreateWorkerVersionPayload) {
+      const resp = await createWorkerVersion(workerId, params)
+      this.workerVersions[resp.id] = resp
+      return resp
+    },
+
     async getWorkerVersion (workerVersionId: UUID) {
       this.workerVersions[workerVersionId] = await retrieveWorkerVersion(workerVersionId)
     },
diff --git a/tests/unit/stores/workers.spec.js b/tests/unit/stores/workers.spec.js
index 079abe1253423ff50f75cbf5c46c01107ffa41c5..16ffaf16af1f0350e66c59bb51b0386a43412086 100644
--- a/tests/unit/stores/workers.spec.js
+++ b/tests/unit/stores/workers.spec.js
@@ -4,7 +4,7 @@ import { pick } from 'lodash'
 import { useWorkerStore, useNotificationStore } from '@/stores'
 import { FakeAxios, setUpTestPinia, actionsCompletedPlugin, assertRejects } from '../testhelpers'
 import { DuplicatedWorker } from '@/api'
-import { workerConfigurationsSample, workersSample, workerSample, workerRunsSample } from '../samples'
+import { workerConfigurationsSample, workersSample, workerVersionsSample, workerSample, workerRunsSample } from '../samples'
 
 describe('workers', () => {
   describe('actions', () => {
@@ -491,5 +491,28 @@ describe('workers', () => {
         })
       })
     })
+
+    describe('createWorkerVersion', () => {
+      it("creates a user's worker version", async () => {
+        const payload = {
+          configuration: {},
+          model_usage: false,
+          gpu_usage: 'disabled'
+        }
+        const sample = workerVersionsSample.results[0]
+        mock.onPost('/workers/workerid/versions/', payload).reply(201, sample)
+
+        await store.createWorkerVersion('workerid', payload)
+        assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url'])), [
+          {
+            method: 'post',
+            url: '/workers/workerid/versions/'
+          }
+        ])
+        assert.deepStrictEqual(store.workerVersions, {
+          [sample.id]: sample
+        })
+      })
+    })
   })
 })