diff --git a/src/api/worker.ts b/src/api/worker.ts
index 2f10cde3cbbdd0b496f7badbdf9b2b3e8f1e48ad..0f8fc10d218cca4bb051ef252ca2b06cfe2c649d 100644
--- a/src/api/worker.ts
+++ b/src/api/worker.ts
@@ -3,12 +3,16 @@ import { PageNumberPaginationParameters, unique } from '.'
 import { PageNumberPagination, UUID } from '@/types'
 import { Worker, WorkerType, WorkerVersion } from '@/types/worker'
 
+export class DuplicatedWorker extends Error {}
+
 export interface ListWorkersParameters extends PageNumberPaginationParameters {
   name?: string
   repository_id?: UUID
   type?: string
 }
 
+export interface CreateWorkerPayload extends Pick<Worker, 'name' | 'slug'> { type: WorkerType['slug'] }
+
 // List executable workers
 export const listWorkers = unique(async (params: ListWorkersParameters): Promise<PageNumberPagination<Worker>> => (await axios.get('/workers/', { params })).data)
 
@@ -27,3 +31,13 @@ export const retrieveWorkerVersion = unique(async (id: UUID): Promise<WorkerVers
 
 // Retrieve a worker
 export const retrieveWorker = unique(async (id: UUID): Promise<Worker> => (await axios.get(`/workers/${id}/`)).data)
+
+/**
+ * Create a local worker.
+ * 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)
+  if (result.status === 200) throw new DuplicatedWorker()
+  return result.data
+})
diff --git a/src/components/Process/Workers/CreateForm.vue b/src/components/Process/Workers/CreateForm.vue
new file mode 100644
index 0000000000000000000000000000000000000000..a8c49c53e62ec77297c0aa4867aaf632e4cf6fe2
--- /dev/null
+++ b/src/components/Process/Workers/CreateForm.vue
@@ -0,0 +1,138 @@
+<template>
+  <button
+    class="button is-primary"
+    v-on:click="openModal = true"
+    v-bind="$attrs"
+  >
+    Create
+  </button>
+
+  <Modal v-model="openModal" title="Create a local worker">
+    <form class="form" v-on:submit.prevent="create">
+      <div class="field">
+        <label class="label">Name</label>
+        <div class="control">
+          <input
+            class="input"
+            :class="{ 'is-danger': fieldErrors.name }"
+            type="text"
+            placeholder="…"
+            maxlength="100"
+            v-model.trim="payload.name"
+          />
+        </div>
+        <p v-if="fieldErrors.name" class="help is-danger">{{ fieldErrors.name.join(', ') }}</p>
+      </div>
+
+      <div class="field">
+        <label class="label">Slug</label>
+        <div class="control">
+          <input
+            class="input"
+            :class="{ 'is-danger': fieldErrors.slug }"
+            type="text"
+            placeholder="…"
+            maxlength="100"
+            v-model.trim="payload.slug"
+          />
+        </div>
+        <p v-if="fieldErrors.slug" class="help is-danger">{{ fieldErrors.slug.join(', ') }}</p>
+      </div>
+
+      <div class="field">
+        <label class="label">Type</label>
+        <div class="control">
+          <input
+            class="input"
+            :class="{ 'is-danger': fieldErrors.type }"
+            type="text"
+            placeholder="…"
+            maxlength="100"
+            v-model.trim="payload.type"
+          />
+        </div>
+        <p v-if="fieldErrors.type" class="help is-danger">{{ fieldErrors.type.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="!canSave"
+        :title="saveTitle"
+      >
+        Create
+      </button>
+    </template>
+  </Modal>
+</template>
+
+<script lang="ts">
+import { mapActions } from 'pinia'
+import { errorParser, ensureArray } from '@/helpers'
+import Modal from '@/components/Modal.vue'
+import { useNotificationStore, useWorkerStore } from '@/stores'
+import { CreateWorkerPayload, DuplicatedWorker } from '@/api'
+import { defineComponent } from 'vue'
+import { isAxiosError } from 'axios'
+
+export default defineComponent({
+  components: {
+    Modal
+  },
+  data: () => ({
+    openModal: false,
+    payload: {
+      name: '',
+      slug: '',
+      type: ''
+    } as CreateWorkerPayload,
+    loading: false,
+    fieldErrors: {} as Partial<Record<keyof CreateWorkerPayload, string[]>>
+  }),
+  computed: {
+    canSave (): boolean {
+      return !Object.values(this.payload).some(v => v === '')
+    },
+    saveTitle () {
+      return this.canSave ? 'Create a new local worker' : 'All fields are required'
+    }
+  },
+  methods: {
+    ...mapActions(useWorkerStore, ['createWorker']),
+    ...mapActions(useNotificationStore, ['notify']),
+    async create () {
+      if (!this.canSave) return
+      this.loading = true
+      this.fieldErrors = {}
+      try {
+        await this.createWorker(this.payload)
+        this.notify({ type: 'info', text: 'Worker created successfully.' })
+        this.openModal = false
+        this.payload = {
+          name: '',
+          slug: '',
+          type: ''
+        }
+      } catch (err) {
+        if (err instanceof DuplicatedWorker) {
+          this.fieldErrors.slug = [`A worker with slug "${this.payload.slug}" already exists`]
+        } else {
+          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
+      }
+    }
+  }
+})
+</script>
diff --git a/src/stores/workers.ts b/src/stores/workers.ts
index acf7e552f6e9f648830dfaaca21af0919ae7ac99..196242c6c0443f5e8346920685850bec78276f94 100644
--- a/src/stores/workers.ts
+++ b/src/stores/workers.ts
@@ -18,7 +18,9 @@ import {
   WorkerConfigurationUpdatePayload,
   retrieveWorkerVersion,
   retrieveWorker,
-  retrieveWorkerRun
+  retrieveWorkerRun,
+  CreateWorkerPayload,
+  createWorker
 } from '@/api'
 import { isAxiosError } from 'axios'
 import { useNotificationStore } from './notification'
@@ -62,6 +64,13 @@ export const useWorkerStore = defineStore('worker', {
       return resp
     },
 
+    async createWorker (params: CreateWorkerPayload) {
+      const resp = await createWorker(params)
+      this.workers[resp.id] = resp
+      // No need to fetch the worker type, as local workers cannot be used to build a worker process
+      return resp
+    },
+
     async listWorkerTypes (params: PageNumberPaginationParameters = {}) {
       const resp = await listWorkerTypes(params)
       this.workerTypes = {
@@ -162,4 +171,4 @@ export const useWorkerStore = defineStore('worker', {
       this.workerConfigurations[data.worker_version.worker.id][data.configuration.id] = data.configuration
     }
   }
-})
\ No newline at end of file
+})
diff --git a/src/views/Process/Workers/List.vue b/src/views/Process/Workers/List.vue
index 9509ef34c43b99dd530a4bcae657f0b608e9091e..190f782f1fed25fefdde59d71123b199f56d86e1 100644
--- a/src/views/Process/Workers/List.vue
+++ b/src/views/Process/Workers/List.vue
@@ -2,6 +2,7 @@
   <main class="container is-fluid">
     <div class="columns">
       <div class="field column is-one-third">
+        <CreateForm class="is-pulled-right" />
         <div class="title is-4">Available Workers</div>
         <div class="field has-addons">
           <!-- Selection of a worker is required to list versions -->
@@ -106,13 +107,15 @@ import VersionList from '@/components/Process/Workers/Versions/List'
 import Paginator from '@/components/Paginator.vue'
 import ListMembers from '@/components/Memberships/ListMembers.vue'
 import WorkerTag from '@/components/Process/Workers/WorkerRuns/WorkerTag'
+import CreateForm from '@/components/Process/Workers/CreateForm.vue'
 
 export default {
   components: {
     Paginator,
     VersionList,
     ListMembers,
-    WorkerTag
+    WorkerTag,
+    CreateForm
   },
   mixins: [
     truncateMixin
diff --git a/tests/unit/stores/workers.spec.js b/tests/unit/stores/workers.spec.js
index 75a0ff3c71bea36b8c170cf8aaa9e42c06eb6b21..42567e2fa2a905737f6d6f0fc7de1ce7452d2c04 100644
--- a/tests/unit/stores/workers.spec.js
+++ b/tests/unit/stores/workers.spec.js
@@ -2,7 +2,8 @@ import { assert } from 'chai'
 import axios from 'axios'
 import { pick } from 'lodash'
 import { useWorkerStore, useNotificationStore } from '@/stores'
-import { FakeAxios, setUpTestPinia, actionsCompletedPlugin } from '../testhelpers'
+import { FakeAxios, setUpTestPinia, actionsCompletedPlugin, assertRejects } from '../testhelpers'
+import { DuplicatedWorker } from '@/api'
 import { workerConfigurationsSample, workersSample, workerSample, workerRunsSample } from '../samples'
 
 describe('workers', () => {
@@ -438,5 +439,46 @@ describe('workers', () => {
         })
       })
     })
+
+    describe('createLocalWorker', () => {
+      it("creates a user's local worker", async () => {
+        const payload = {
+          name: workerSample.name,
+          type: workerSample.type,
+          slug: workerSample.slug
+        }
+        mock.onPost('/workers/', payload).reply(201, workerSample)
+
+        await store.createWorker(payload)
+        assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url'])), [
+          {
+            method: 'post',
+            url: '/workers/'
+          }
+        ])
+        assert.deepStrictEqual(store.workers, {
+          [workerSample.id]: workerSample
+        })
+      })
+
+      it('throws a specific error in case the worker already exists', async () => {
+        const payload = {
+          name: workerSample.name,
+          type: workerSample.type,
+          slug: workerSample.slug
+        }
+        mock.onPost('/workers/', payload).reply(200, workerSample)
+
+        const err = await assertRejects(async () => { await store.createWorker(payload) })
+        assert.ok(err instanceof DuplicatedWorker)
+        assert.deepStrictEqual(mock.history.all.map(req => pick(req, ['method', 'url'])), [
+          {
+            method: 'post',
+            url: '/workers/'
+          }
+        ])
+        assert.deepStrictEqual(store.workers, {})
+      })
+    })
   })
 })