diff --git a/src/components/Element/Classifications/Classification.vue b/src/components/Element/Classifications/Classification.vue
index 1366605a427ed961b6a3c665ca61c66880cc0ff4..b18211f130bbb5de617047d1e21e87631196c157 100644
--- a/src/components/Element/Classifications/Classification.vue
+++ b/src/components/Element/Classifications/Classification.vue
@@ -1,5 +1,5 @@
 <template>
-  <div class="columns mr-0">
+  <div class="columns mt-0 mr-0">
     <div class="column">
       {{ name }}
       <i
diff --git a/src/components/Element/Classifications/Classifications.vue b/src/components/Element/Classifications/Classifications.vue
index 10bab75d4d02f42511bce2e5cb450ba3a2cd192f..dec112651a56b0039edc1ebfa109ff36e7834bd5 100644
--- a/src/components/Element/Classifications/Classifications.vue
+++ b/src/components/Element/Classifications/Classifications.vue
@@ -1,16 +1,44 @@
 <template>
   <section class="mb-4">
+    <div class="mt-2 pb-5" v-if="manualClassifications.length">
+      <strong class="mb-4">Manual</strong>
+      <Classification
+        v-for="classification in manualClassifications"
+        :key="classification.id"
+        :classification="classification"
+        :disabled="!canWrite(corpus)"
+        :element-id="element.id"
+      />
+    </div>
+
     <div
       class="mt-2 pb-5"
-      v-for="(results, version) in groupedClassifications"
-      :key="version"
+      v-for="[workerRunId, classifications] in workerRunClassifications"
+      :key="workerRunId"
+    >
+      <WorkerRunSummary
+        class="mb-4"
+        :worker-run-details="workerRunSummaries[workerRunId]"
+      />
+      <Classification
+        v-for="classification in classifications"
+        :key="classification.id"
+        :classification="classification"
+        :disabled="!canWrite(corpus)"
+        :element-id="element.id"
+      />
+    </div>
+
+    <div
+      class="mt-2 pb-5"
+      v-for="[versionId, classifications] in workerVersionClassifications"
+      :key="versionId"
     >
       <div class="mb-4">
-        <strong v-if="version === MANUAL_WORKER_VERSION">Manual</strong>
-        <WorkerVersionDetails v-else :worker-version-id="version" has-outside-title />
+        <WorkerVersionDetails :worker-version-id="versionId" has-outside-title />
       </div>
       <Classification
-        v-for="classification in results"
+        v-for="classification in classifications"
         :key="classification.id"
         :classification="classification"
         :disabled="!canWrite(corpus)"
@@ -22,41 +50,80 @@
 
 <script>
 import { orderBy, groupBy } from 'lodash'
-import Classification from './Classification'
+
 import { corporaMixin } from '@/mixins.js'
-import { MANUAL_WORKER_VERSION } from '@/config.js'
+
+import WorkerRunSummary from '@/components/Process/Workers/WorkerRunSummary'
 import WorkerVersionDetails from '@/components/Process/Workers/Versions/Details.vue'
+import Classification from './Classification'
+
 export default {
   mixins: [
     corporaMixin
   ],
   components: {
     Classification,
-    WorkerVersionDetails
+    WorkerVersionDetails,
+    WorkerRunSummary
   },
   props: {
     element: {
       type: Object,
-      required: true
+      required: true,
+      validator: element => Array.isArray(element.classifications)
     }
   },
-  data: () => ({
-    MANUAL_WORKER_VERSION
-  }),
   computed: {
     corpusId () {
       return this.element.corpus.id
     },
     /**
-     * Group classifications by worker version and order by confidence.
-     * Returns { worker version ID: [classifications] }
-     * For manual classifications, since `null` cannot be set as an object's key,
-     * the key `__manual__` is used.
+     * Classifications sorted by descending confidence and ascending class name.
+     * This sorting is computed once and then re-used by other computed properties
+     * to perform the grouping.
+     */
+    sortedClassifications () {
+      return orderBy(
+        this.element.classifications,
+        ['confidence', 'class_name'],
+        ['desc', 'asc']
+      )
+    },
+    manualClassifications () {
+      return this.sortedClassifications.filter(classification => !classification.worker_run && !classification.worker_version)
+    },
+    /**
+     * Classifications with worker runs, grouped by their worker run ID and sorted by their worker run summary.
+     * @returns {[string, object[]][]}
+     */
+    workerRunClassifications () {
+      const grouped = groupBy(
+        this.sortedClassifications.filter(classification => classification.worker_run),
+        'worker_run.id'
+      )
+      return orderBy(Object.entries(grouped), ([id]) => this.workerRunSummaries[id])
+    },
+    /**
+     * Worker run summary serializers mapped to their IDs.
+     * @returns {{ [id: string]: { id: string, summary: string } }}
+     */
+    workerRunSummaries () {
+      return Object.fromEntries(
+        this.sortedClassifications
+          .filter(classification => classification?.worker_run)
+          .map(classification => [classification.worker_run.id, classification.worker_run])
+      )
+    },
+    /**
+     * Classifications with worker versions and no worker runs, grouped by their worker version ID and sorted only by ID.
+     * @returns {[string, object[]][]}
      */
-    groupedClassifications () {
-      if (!this.element || !this.element.classifications) return {}
-      const orderedClassifications = orderBy(this.element.classifications, [c => c.confidence], ['desc'])
-      return groupBy(orderedClassifications, c => c.worker_version || MANUAL_WORKER_VERSION)
+    workerVersionClassifications () {
+      const grouped = groupBy(
+        this.sortedClassifications.filter(classification => classification.worker_version && !classification.worker_run),
+        'worker_version'
+      )
+      return orderBy(Object.entries(grouped), [0])
     }
   }
 }
diff --git a/src/components/Element/DetailsPanel.vue b/src/components/Element/DetailsPanel.vue
index 3754d68ddc669fc1b835b0debfaa062ba8c84984..b00a7099b15578f0c59516df7862838cd2bc14ad 100644
--- a/src/components/Element/DetailsPanel.vue
+++ b/src/components/Element/DetailsPanel.vue
@@ -43,13 +43,7 @@
 
       <template v-if="elementType.folder === false">
         <DropdownContent id="transcriptions" title="Transcriptions">
-          <GroupedTranscriptions
-            v-for="[workerId, transcriptions] in groupedTranscriptions"
-            :key="workerId"
-            :element="element"
-            :transcriptions="transcriptions"
-            :worker-id="workerId"
-          />
+          <Transcriptions :element="element" />
           <template v-if="canWriteElement(elementId)">
             <div class="has-text-right">
               <a v-on:click="transcriptionModal = true">
@@ -88,35 +82,33 @@
 </template>
 
 <script>
-import { mapState, mapActions, mapGetters } from 'vuex'
+import { mapState, mapGetters } from 'vuex'
 import { corporaMixin } from '@/mixins.js'
-import { MANUAL_WORKER_VERSION } from '@/config.js'
-import { groupBy, orderBy } from 'lodash'
 
-import GroupedTranscriptions from './Transcription'
+import DropdownContent from '@/components/DropdownContent.vue'
+import MLClassSelect from '@/components/MLClassSelect.vue'
 import EntityLinks from '@/components/Entity/Links.vue'
 import Classifications from './Classifications'
-import MLClassSelect from '@/components/MLClassSelect.vue'
 import ElementMetadata from './Metadata'
-import DropdownContent from '@/components/DropdownContent.vue'
+import OrientationPanel from './OrientationPanel'
+import Transcriptions from './Transcription'
 import TranscriptionsModal from './Transcription/Modal'
 import TranscriptionCreationForm from './Transcription/CreationForm'
-import OrientationPanel from './OrientationPanel'
 
 export default {
   mixins: [
     corporaMixin
   ],
   components: {
-    GroupedTranscriptions,
-    EntityLinks,
     Classifications,
-    MLClassSelect,
-    ElementMetadata,
     DropdownContent,
-    TranscriptionsModal,
+    ElementMetadata,
+    EntityLinks,
+    MLClassSelect,
+    OrientationPanel,
     TranscriptionCreationForm,
-    OrientationPanel
+    Transcriptions,
+    TranscriptionsModal
   },
   props: {
     elementId: {
@@ -169,31 +161,9 @@ export default {
       const neighbor = this.neighbors?.[this.elementId]?.results?.find(n => n.element?.id === this.elementId)
       // Prevent errors on null parents since ListElementNeighbors can return null parents for paths with 'ghost elements'
       return [...neighbor?.parents ?? []].reverse().find(parent => parent !== null)?.id
-    },
-    groupedTranscriptions () {
-      // Find all transcriptions attached to this element
-      let transcriptions = Object.values(this.transcriptions[this.elementId] ?? {})
-      if (!transcriptions.length) return []
-
-      /*
-       * Skip all transcriptions that have a worker version ID, but the worker version is not yet loaded.
-       * A watcher will handle the actual loading of worker versions, and this computed should update itself.
-       */
-      transcriptions = transcriptions.filter(t => !t.worker_version_id || this.workerVersions[t.worker_version_id])
-
-      // Group transcriptions by worker
-      const grouped = groupBy(transcriptions, t => {
-        if (!t.worker_version_id) return MANUAL_WORKER_VERSION
-        return this.workerVersions[t.worker_version_id].worker.id
-      })
-
-      // Order by worker name
-      return orderBy(Object.entries(grouped), ([id]) => id === MANUAL_WORKER_VERSION ? '' : this.workers[id].name)
     }
   },
   methods: {
-    ...mapActions('elements', ['listTranscriptions']),
-    ...mapActions('process', ['getWorkerVersion']),
     async createClassification () {
       if (!this.canCreateClassification) return
       this.isSavingNewClassification = true
@@ -206,23 +176,12 @@ export default {
         this.$refs.newClassificationSelect.clear()
         this.isSavingNewClassification = false
       }
-    },
-    async fetchTranscriptionWorkerVersions (elementId) {
-      if (!this.transcriptions[elementId]) return
-      [...new Set(
-        Object.values(this.transcriptions[elementId])
-          // Get all the worker version IDs of all transcriptions on this element
-          .map(transcription => transcription.worker_version_id)
-          // If the worker version ID is not null (not manual) and the version ID was not loaded in the frontend
-          .filter(id => id && !(id in this.workerVersions))
-        // Retrieve the worker version's information
-      )].forEach(id => this.getWorkerVersion(id))
     }
   },
   watch: {
     elementId: {
       immediate: true,
-      async handler (id) {
+      handler (id) {
         if (!id) return
         /*
          * Do not retrieve the element again if it already exists in the store,
@@ -231,18 +190,7 @@ export default {
          * This ensures there are no strange behaviors where some actions are only sometimes disabled when they shouldn't,
          * or some element attributes are not displayed at all.
          */
-        if (!this.element || this.element.id !== id || !this.element.rights || !this.element.classifications) await this.$store.dispatch('elements/get', { id })
-        await this.listTranscriptions({ id })
-        this.fetchTranscriptionWorkerVersions(id)
-      }
-    },
-    elementType: {
-      async handler (type) {
-        // List transcriptions attached to this element
-        if (type.folder === false && this.transcriptions[this.elementId] === undefined) {
-          await this.listTranscriptions({ id: this.elementId })
-          this.fetchTranscriptionWorkerVersions(this.elementId)
-        }
+        if (!this.element || this.element.id !== id || !this.element.rights || !this.element.classifications) this.$store.dispatch('elements/get', { id })
       }
     }
   }
diff --git a/src/components/Element/PanelHeader.vue b/src/components/Element/PanelHeader.vue
index 7fb3b4c3e928e75c946d1a6ff5efff5b5c56664a..46f673127582ca37c765dc551f653c29d9cea468 100644
--- a/src/components/Element/PanelHeader.vue
+++ b/src/components/Element/PanelHeader.vue
@@ -1,56 +1,53 @@
 <template>
   <div v-if="element" class="mb-3">
-    <div class="columns is-flex-grow-1">
-      <div class="column">
-        <span class="subtitle is-5">
-          <span :title="element.type" class="has-text-grey mr-1">{{ typeName(element.type) }}</span>
-          <strong :title="element.name">{{ element.name }}</strong>
+    <div class="is-pulled-right ml-2" v-if="isAnnotable">
+      <button
+        v-if="!annotationEnabled"
+        class="button is-primary"
+        title="Open annotation panel"
+        v-on:click="toggle(true)"
+      >
+        <span class="icon">
+          <i class="icon-plus"></i>
         </span>
-        <router-link
-          v-if="element && element.id !== mainElementId"
-          :to="{ name: 'element-details', params: { id: element.id } }"
-        >
-          <i class="icon-link" :title="`Navigate to ${element.name}`"></i>
-        </router-link>
-        <a
-          class="icon-trash"
-          :class="canAdminElement(elementId) ? 'has-text-danger' : 'has-text-grey-light'"
-          :title="canAdminElement(elementId) ? 'Delete this element' : 'Admin access on the project is required to delete this element'"
-          v-on:click="deleteModal = canAdminElement(elementId)"
-        ></a>
-        <p>
-          <!-- Allow both worker_version and worker_version_id because the list and retrieve endpoints are inconsistent -->
-          <WorkerVersionDetails
-            v-if="element.worker_version || element.worker_version_id"
-            :worker-version-id="element.worker_version || element.worker_version_id"
-            has-outside-title
-          />
-          <span v-else-if="element.creator">Created by <strong>{{ element.creator }}</strong></span>
-          <ConfidenceTag v-if="Number.isFinite(element.confidence)" :value="element.confidence" />
-        </p>
-      </div>
-      <div class="column is-narrow" v-if="isAnnotable">
-        <button
-          v-if="!annotationEnabled"
-          class="button is-primary"
-          title="Open annotation panel"
-          v-on:click="toggle(true)"
-        >
-          <span class="icon">
-            <i class="icon-plus"></i>
-          </span>
-          <span>annotate</span>
-        </button>
-        <button
-          v-else-if="annotationEnabled"
-          class="button is-danger"
-          title="Close annotation panel"
-          v-on:click="toggle(false)"
-        >
-          <span>close</span>
-        </button>
-      </div>
+        <span>annotate</span>
+      </button>
+      <button
+        v-else-if="annotationEnabled"
+        class="button is-danger"
+        title="Close annotation panel"
+        v-on:click="toggle(false)"
+      >
+        <span>close</span>
+      </button>
     </div>
+    <span class="subtitle is-5">
+      <span :title="element.type" class="has-text-grey mr-1">{{ typeName(element.type) }}</span>
+      <strong :title="element.name">{{ element.name }}</strong>
+    </span>
+    <router-link
+      v-if="element && element.id !== mainElementId"
+      :to="{ name: 'element-details', params: { id: element.id } }"
+    >
+      <i class="icon-link" :title="`Navigate to ${element.name}`"></i>
+    </router-link>
+    <a
+      class="icon-trash"
+      :class="canAdminElement(elementId) ? 'has-text-danger' : 'has-text-grey-light'"
+      :title="canAdminElement(elementId) ? 'Delete this element' : 'Admin access on the project is required to delete this element'"
+      v-on:click="deleteModal = canAdminElement(elementId)"
+    ></a>
+    <p>
+      <WorkerRunSummary v-if="element.worker_run" :worker-run-details="element.worker_run" />
+      <!-- Allow both worker_version and worker_version_id because the list and retrieve endpoints are inconsistent -->
+      <WorkerVersionDetails
+        v-else-if="element.worker_version || element.worker_version_id"
+        :worker-version-id="element.worker_version || element.worker_version_id"
+        has-outside-title
+      />
+      <span v-else-if="element.creator">Created by <strong>{{ element.creator }}</strong></span>
+      <ConfidenceTag v-if="Number.isFinite(element.confidence)" :value="element.confidence" />
+    </p>
     <Modal v-model="deleteModal" :title="'Delete ' + typeName(element.type) + ' ' + element.name">
       <p>
         Are you sure you want to delete the {{ typeName(element.type) }}
@@ -72,6 +69,7 @@
 import { mapState, mapGetters, mapMutations } from 'vuex'
 import { corporaMixin } from '@/mixins.js'
 
+import WorkerRunSummary from '@/components/Process/Workers/WorkerRunSummary.vue'
 import WorkerVersionDetails from '@/components/Process/Workers/Versions/Details.vue'
 import Modal from '@/components/Modal.vue'
 import ConfidenceTag from '@/components/ConfidenceTag.vue'
@@ -83,7 +81,8 @@ export default {
   components: {
     WorkerVersionDetails,
     Modal,
-    ConfidenceTag
+    ConfidenceTag,
+    WorkerRunSummary
   },
   props: {
     elementId: {
diff --git a/src/components/Element/Transcription/Box.vue b/src/components/Element/Transcription/Box.vue
index b5106341e0cc4727c924d5e13315c6cc2cfa0e7c..f282a6c6fbef5e5b10c0f957ccbc73349a20ad82 100644
--- a/src/components/Element/Transcription/Box.vue
+++ b/src/components/Element/Transcription/Box.vue
@@ -4,7 +4,7 @@
     :style="orientationStyle(transcription.orientation)"
   >
     <blockquote class="transcription-box">
-      <template v-if="entities.length">
+      <template v-if="transcriptionEntities.length">
         <Token
           v-for="(token, index) in tokens"
           v-bind="token"
@@ -28,9 +28,24 @@ export default {
       type: Object,
       required: true
     },
+    /**
+     * Filter TranscriptionEntities that do not have a WorkerRun by WorkerVersion.
+     * If both this parameter and the WorkerRun filter are unset, no TranscriptionEntities are displayed.
+     * To display entities created without a WorkerVersion or a WorkerRun, use the `MANUAL_WORKER_VERSION` const.
+     * Both parameters may not be set at once.
+     */
     workerVersionFilter: {
       type: String,
       default: ''
+    },
+    /**
+     * Filter TranscriptionEntities by WorkerRun UUID.
+     * If both this parameter and the WorkerVersion filter are unset, no TranscriptionEntities are displayed.
+     * Both parameters may not be set at once.
+     */
+    workerRunFilter: {
+      type: String,
+      default: ''
     }
   },
   components: {
@@ -39,19 +54,26 @@ export default {
   computed: {
     ...mapState('entity', ['inTranscription']),
     loaded () {
-      if (!this.inTranscription || !this.inTranscription[this.transcription.id]) return false
-      const { results, count, loaded } = this.inTranscription[this.transcription.id]
-      return results && count === loaded
+      const { results, count, loaded } = this.inTranscription?.[this.transcription.id] ?? {}
+      return results && loaded >= count
     },
-    entities () {
-      if (!this.loaded || !this.workerVersionFilter) return []
-      return this
-        .inTranscription[this.transcription.id]
-        .results
-        .filter(transcriptionEntity => (transcriptionEntity.worker_version_id || MANUAL_WORKER_VERSION) === this.workerVersionFilter)
+    transcriptionEntities () {
+      let transcriptionEntities = this.inTranscription?.[this.transcription.id]?.results
+
+      if (!this.loaded || !transcriptionEntities?.length || (!this.workerVersionFilter && !this.workerRunFilter)) return []
+      if (this.workerVersionFilter && this.workerRunFilter) throw new Error('The WorkerVersion and WorkerRun filters cannot be used at the same time.')
+
+      if (this.workerVersionFilter) {
+        transcriptionEntities = transcriptionEntities.filter(transcriptionEntity =>
+          !transcriptionEntity.worker_run &&
+          (transcriptionEntity.worker_version_id || MANUAL_WORKER_VERSION) === this.workerVersionFilter
+        )
+      } else if (this.workerRunFilter) transcriptionEntities = transcriptionEntities.filter(transcriptionEntity => transcriptionEntity.worker_run?.id === this.workerRunFilter)
+
+      return transcriptionEntities
     },
     tokens () {
-      return parseEntities(this.transcription.text, this.entities)
+      return parseEntities(this.transcription.text, this.transcriptionEntities)
     }
   },
   methods: {
diff --git a/src/components/Element/Transcription/GroupedTranscriptions.vue b/src/components/Element/Transcription/GroupedTranscriptions.vue
deleted file mode 100644
index 912b2cef3a7f0e20bf1e90210c65f5c0284e246c..0000000000000000000000000000000000000000
--- a/src/components/Element/Transcription/GroupedTranscriptions.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<template>
-  <section>
-    <div
-      class="pb-4"
-      v-for="(results, version) in groupedTranscriptions"
-      :key="version"
-    >
-      <div class="mb-2">
-        <strong v-if="version === MANUAL_WORKER_VERSION">Manual</strong>
-        <WorkerVersionDetails v-else :worker-version-id="version" has-outside-title />
-      </div>
-      <Transcription
-        v-for="transcription in results"
-        :key="transcription.id"
-        :element="element"
-        :transcription="transcription"
-      />
-    </div>
-  </section>
-</template>
-
-<script>
-import { mapState } from 'vuex'
-import { orderBy, groupBy } from 'lodash'
-import { MANUAL_WORKER_VERSION } from '@/config.js'
-import WorkerVersionDetails from '@/components/Process/Workers/Versions/Details.vue'
-import Transcription from './Transcription'
-
-export default {
-  props: {
-    element: {
-      type: Object,
-      required: true
-    },
-    transcriptions: {
-      type: Array,
-      required: true
-    },
-    workerId: {
-      type: String,
-      required: true
-    }
-  },
-  data: () => ({
-    MANUAL_WORKER_VERSION
-  }),
-  components: {
-    Transcription,
-    WorkerVersionDetails
-  },
-  methods: {
-    getCreatedDate (versionId) {
-      if (!versionId) return
-      return new Date(this.workerVersions[versionId].revision.created)
-    }
-  },
-  computed: {
-    ...mapState('process', ['workerVersions']),
-    groupedTranscriptions () {
-      const orderedTranscriptions = orderBy(this.transcriptions, t => this.getCreatedDate(t.worker_version_id), 'desc')
-      return groupBy(orderedTranscriptions, t => t.worker_version_id || MANUAL_WORKER_VERSION)
-    }
-  }
-}
-</script>
diff --git a/src/components/Element/Transcription/Transcription.vue b/src/components/Element/Transcription/Transcription.vue
index e594468ebe045893985da346b9b0f6736e9d5ac4..a73b94905e707edfeea3966a1941dc8758e0b52e 100644
--- a/src/components/Element/Transcription/Transcription.vue
+++ b/src/components/Element/Transcription/Transcription.vue
@@ -13,26 +13,37 @@
         :transcription="transcription"
         v-on:edit="editing = true"
       />
-      <div class="select" v-if="entityVersions?.length">
-        <select v-model="workerVersionFilter">
+      <div class="select is-truncated" v-if="entityFilterValues.length">
+        <select v-model="filterValue">
           <option value="">No entities</option>
-          <option v-for="[id, name] in entityVersions" :key="id" :value="id">{{ name }}</option>
+          <option v-for="[id, name] in entityFilterValues" :key="id" :value="id">{{ truncateSelect(name) }}</option>
         </select>
       </div>
       <span class="is-clearfix"></span>
-      <Box :transcription="transcription" :worker-version-filter="workerVersionFilter" />
+      <Box
+        :transcription="transcription"
+        :worker-version-filter="workerVersionFilter"
+        :worker-run-filter="workerRunFilter"
+      />
     </template>
   </div>
 </template>
 
 <script>
+import { sortBy } from 'lodash'
 import { mapState, mapActions } from 'vuex'
+
 import { MANUAL_WORKER_VERSION } from '@/config.js'
+import { truncateMixin } from '@/mixins'
+
 import Actions from './Actions'
 import Box from './Box'
 import EditionForm from './EditionForm'
 
 export default {
+  mixins: [
+    truncateMixin
+  ],
   props: {
     element: {
       type: Object,
@@ -50,6 +61,7 @@ export default {
   },
   data: () => ({
     workerVersionFilter: '',
+    workerRunFilter: '',
     editing: false
   }),
   mounted () {
@@ -58,20 +70,75 @@ export default {
   computed: {
     ...mapState('entity', ['inTranscription']),
     ...mapState('process', ['workerVersions']),
+
+    /**
+     * Values and display names for the options of the entity worker versions/worker runs filter.
+     * @returns {[string, string][]} Array of IDs and display names.
+     *   The IDs are UUIDs that start with a `run-` prefix for WorkerRuns and a `version-` prefix for WorkerVersions.
+     */
+    entityFilterValues () {
+      const transcriptionEntities = this.inTranscription?.[this.transcription.id]?.results
+      if (!transcriptionEntities) return []
+
+      const values = []
+      // If there are TranscriptionEntities with no worker run and no worker version, add the manual option as the very first item
+      if (transcriptionEntities.some(transcriptionEntity => !transcriptionEntity.worker_version_id && !transcriptionEntity.worker_run)) {
+        values.push([`version-${MANUAL_WORKER_VERSION}`, 'Manual'])
+      }
+
+      values.push(
+        ...sortBy(
+          // Turn into an object to make the array unique by ID, but turn back into an array to allow sorting
+          Object.entries(Object.fromEntries(
+            transcriptionEntities
+              .filter(transcriptionEntity => transcriptionEntity.worker_run)
+              .map(transcriptionEntity => ([`run-${transcriptionEntity.worker_run.id}`, transcriptionEntity.worker_run.summary])),
+            [1, 0]
+          ))
+        )
+      )
+
+      values.push(
+        ...sortBy(
+          Object.entries(Object.fromEntries(
+            transcriptionEntities
+              .filter(transcriptionEntity => !transcriptionEntity.worker_run && transcriptionEntity.worker_version_id)
+              .map(transcriptionEntity => this.workerVersions[transcriptionEntity.worker_version_id])
+              // Ignore worker versions that were not yet loaded
+              .filter(version => version)
+              .map(version => ([`version-${version.id}`, `${version.worker.name} ${version.revision.hash.substring(0, 8)}`]))
+          )),
+          [1, 0]
+        )
+      )
+
+      return values
+    },
+
     /**
-     * Values and display names for the options of the entity worker versions filter.
-     * @return {[string, string][]} Array of [worker version ID, worker version display name]
+     * Since it is not possible for the options of a <select> to use anything other than a single string
+     * as a value without causing an infinite stream of bugs, we use strings prefixed with "run-" or "version-"
+     * depending on whether we should filter by WorkerRun or by WorkerVersion.
+     * This computed property makes the link between the selected option value and the actual filter attributes.
      */
-    entityVersions () {
-      if (!this.inTranscription?.[this.transcription.id]?.results) return []
-      // Build a unique set of all worker version IDs
-      const ids = new Set(this.inTranscription[this.transcription.id].results.map(transcriptionEntity => transcriptionEntity.worker_version_id))
-      // Ignore worker versions that are not yet loaded; a watcher loads those
-      return [...ids].filter(id => !id || this.workerVersions[id]).map(id => {
-        if (!id) return [MANUAL_WORKER_VERSION, 'Manual']
-        const version = this.workerVersions[id]
-        return [id, version.worker.name + ' ' + version.revision.hash.substring(0, 8)]
-      })
+    filterValue: {
+      get () {
+        if (this.workerRunFilter) return `run-${this.workerRunFilter}`
+        else if (this.workerVersionFilter) return `version-${this.workerVersionFilter}`
+        else return ''
+      },
+      set (newValue) {
+        if (!newValue) {
+          this.workerVersionFilter = ''
+          this.workerRunFilter = ''
+        } else if (newValue.startsWith('run-')) {
+          this.workerVersionFilter = ''
+          this.workerRunFilter = newValue.slice(4)
+        } else if (newValue.startsWith('version-')) {
+          this.workerVersionFilter = newValue.slice(8)
+          this.workerRunFilter = ''
+        } else throw new Error(`Unsupported filter value ${newValue}`)
+      }
     }
   },
   methods: {
@@ -82,22 +149,30 @@ export default {
       const transcriptionEntities = newValue?.[this.transcription.id]?.results
       if (!transcriptionEntities?.length) return
       // Get every worker version ID of every TranscriptionEntity
-      [...new Set(transcriptionEntities.map(transcriptionEntity => transcriptionEntity.worker_version_id))]
+      [...new Set(
+        transcriptionEntities
+          // Ignore those with worker runs, as we will not use their version
+          .filter(transcriptionEntity => !transcriptionEntity.worker_run)
+          .map(transcriptionEntity => transcriptionEntity.worker_version_id)
+      )]
         // Only pick those that are not yet in the store
         .filter(id => id && !this.workerVersions[id])
         // Fetch them
         .map(id => this.getWorkerVersion(id))
     },
-    versionIds (newValue) {
-      // Automatically select the first available worker version
-      if (newValue.length) this.workerVersionFilter = newValue[0][0]
+    entityFilterValues: {
+      handler (newValue) {
+        // Automatically select the first available filter option
+        if (newValue.length) this.filterValue = newValue[0][0]
+      },
+      immediate: true
     }
   }
 }
 </script>
 
 <style scoped>
-/* Add some spacing between two transcriptions with the same worker version */
+/* Add some spacing between two transcriptions */
 .transcription:not(:last-child) {
   margin-bottom: .5rem;
 }
diff --git a/src/components/Element/Transcription/Transcriptions.vue b/src/components/Element/Transcription/Transcriptions.vue
new file mode 100644
index 0000000000000000000000000000000000000000..7813e82e3ac5f1c03aa6c7a878def14811d4c4bd
--- /dev/null
+++ b/src/components/Element/Transcription/Transcriptions.vue
@@ -0,0 +1,161 @@
+<template>
+  <section v-if="transcriptions[element.id]">
+    <div class="pb-4" v-if="manualTranscriptions.length">
+      <strong class="mb-2">Manual</strong>
+      <Transcription
+        v-for="transcription in manualTranscriptions"
+        :key="transcription.id"
+        :element="element"
+        :transcription="transcription"
+      />
+    </div>
+
+    <div
+      class="pb-4"
+      v-for="[workerRunId, transcriptions] in workerRunTranscriptions"
+      :key="workerRunId"
+    >
+      <WorkerRunSummary
+        class="mb-2"
+        :worker-run-details="workerRunSummaries[workerRunId]"
+      />
+      <Transcription
+        v-for="transcription in transcriptions"
+        :key="transcription.id"
+        :element="element"
+        :transcription="transcription"
+      />
+    </div>
+
+    <div
+      class="pb-4"
+      v-for="[versionId, transcriptions] in workerVersionTranscriptions"
+      :key="versionId"
+    >
+      <div class="mb-2">
+        <WorkerVersionDetails :worker-version-id="versionId" has-outside-title />
+      </div>
+      <Transcription
+        v-for="transcription in transcriptions"
+        :key="transcription.id"
+        :element="element"
+        :transcription="transcription"
+      />
+    </div>
+  </section>
+</template>
+
+<script>
+import { mapActions, mapState } from 'vuex'
+import { orderBy, groupBy } from 'lodash'
+
+import WorkerRunSummary from '@/components/Process/Workers/WorkerRunSummary.vue'
+import WorkerVersionDetails from '@/components/Process/Workers/Versions/Details.vue'
+import Transcription from './Transcription'
+
+export default {
+  props: {
+    element: {
+      type: Object,
+      required: true,
+      validator: value => value?.id
+    }
+  },
+  components: {
+    Transcription,
+    WorkerVersionDetails,
+    WorkerRunSummary
+  },
+  computed: {
+    ...mapState('elements', ['transcriptions']),
+    ...mapState('process', ['workerVersions']),
+    /**
+     * Transcriptions sorted by descending confidence and ascending text.
+     * This sorting is computed once and then re-used by other computed properties
+     * to perform the grouping.
+     */
+    sortedTranscriptions () {
+      return orderBy(
+        this.transcriptions[this.element.id],
+        ['confidence', 'text'],
+        ['desc', 'asc']
+      )
+    },
+    manualTranscriptions () {
+      return this.sortedTranscriptions.filter(transcription => !transcription.worker_run && !transcription.worker_version_id)
+    },
+    /**
+     * Transcriptions with worker runs, grouped by their worker run ID and sorted by their worker run summary.
+     * @returns {[string, object[]][]}
+     */
+    workerRunTranscriptions () {
+      const grouped = groupBy(
+        this.sortedTranscriptions.filter(transcription => transcription.worker_run),
+        'worker_run.id'
+      )
+      return orderBy(Object.entries(grouped), ([id]) => this.workerRunSummaries[id])
+    },
+    /**
+     * Worker run summary serializers mapped to their IDs.
+     * @returns {{ [id: string]: { id: string, summary: string } }}
+     */
+    workerRunSummaries () {
+      return Object.fromEntries(
+        this.sortedTranscriptions
+          .filter(transcription => transcription?.worker_run)
+          .map(transcription => [transcription.worker_run.id, transcription.worker_run])
+      )
+    },
+    /**
+     * Transcriptions with worker versions and no worker runs, grouped by their worker version ID
+     * and sorted by worker name, then revision creation date, then worker version ID.
+     * If the worker version is not yet loaded, this only sorts by worker version ID.
+     * @returns {[string, object[]][]}
+     */
+    workerVersionTranscriptions () {
+      const grouped = groupBy(
+        this.sortedTranscriptions.filter(transcription => transcription.worker_version_id && !transcription.worker_run),
+        'worker_version_id'
+      )
+      return orderBy(Object.entries(grouped), [
+        ([id]) => this.workerVersions[id]?.worker?.name,
+        ([id]) => this.workerVersions[id]?.revision?.created,
+        /*
+         * Fallback to sorting by worker version ID. When the worker version is not yet loaded,
+         * the two functions above will return `null`, and this will be the only sorting criterion.
+         */
+        '0'
+      ])
+    }
+  },
+  methods: {
+    ...mapActions('elements', ['listTranscriptions']),
+    ...mapActions('process', ['getWorkerVersion'])
+  },
+  watch: {
+    element: {
+      immediate: true,
+      async handler (newValue) {
+        if (!newValue) return
+        if (!this.transcriptions[newValue.id]) await this.listTranscriptions({ id: newValue.id })
+
+        /*
+         * Once transcriptions are loaded, we need to fetch all worker versions
+         * to properly sort the transcriptions that have no worker runs and preserve the older sorting method.
+         * TODO: Remove this whole fetching once we fully switch to worker runs
+         */
+        if (!this.transcriptions[newValue.id]) return
+        [...new Set(
+          Object.values(this.transcriptions[newValue.id])
+            // Get all the worker version IDs of all transcriptions on this element that have no workerRuns
+            .filter(transcription => !transcription.worker_run)
+            .map(transcription => transcription.worker_version_id)
+            // If the worker version ID is not null (not manual) and the version ID was not loaded in the frontend
+            .filter(id => id && !(id in this.workerVersions))
+          // Retrieve the worker version's information
+        )].forEach(id => this.getWorkerVersion(id))
+      }
+    }
+  }
+}
+</script>
diff --git a/src/components/Element/Transcription/index.js b/src/components/Element/Transcription/index.js
index 9d345fa1bb2b97c9c48efb354163e4040cb22fe0..93df33af45bef33c52c68aef23804f83b5241917 100644
--- a/src/components/Element/Transcription/index.js
+++ b/src/components/Element/Transcription/index.js
@@ -1 +1 @@
-export { default } from './GroupedTranscriptions'
+export { default } from './Transcriptions'
diff --git a/src/components/Process/Workers/WorkerRunSummary.vue b/src/components/Process/Workers/WorkerRunSummary.vue
new file mode 100644
index 0000000000000000000000000000000000000000..0342dc4e3c2870504aa8833f9db134cb0b2d7883
--- /dev/null
+++ b/src/components/Process/Workers/WorkerRunSummary.vue
@@ -0,0 +1,26 @@
+<template>
+  <div :title="workerRunDetails.summary">
+    <span class="summary">{{ workerRunDetails.summary }}</span>
+  </div>
+</template>
+
+<script>
+export default {
+  props: {
+    workerRunDetails: {
+      type: Object,
+      required: true,
+      validator: value => value?.id && value?.summary
+    }
+  }
+}
+</script>
+
+<style scoped>
+.summary {
+  display: block;
+  white-space: nowrap;
+  overflow: hidden;
+  text-overflow: ellipsis;
+}
+</style>