diff --git a/components/CommonActionForm.vue b/components/CommonActionForm.vue
new file mode 100644
index 0000000000000000000000000000000000000000..8d76634794b94b52c1c7544b5d5497eabbde0bb2
--- /dev/null
+++ b/components/CommonActionForm.vue
@@ -0,0 +1,240 @@
+<!--
+Web client of the Sensor Management System software developed within the
+Helmholtz DataHub Initiative by GFZ and UFZ.
+
+Copyright (C) 2020, 2021
+- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+- Helmholtz Centre Potsdam - GFZ German Research Centre for
+  Geosciences (GFZ, https://www.gfz-potsdam.de)
+
+Parts of this program were developed within the context of the
+following publicly funded projects or measures:
+- Helmholtz Earth and Environment DataHub
+  (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+
+Licensed under the HEESIL, Version 1.0 or - as soon they will be
+approved by the "Community" - subsequent versions of the HEESIL
+(the "Licence").
+
+You may not use this work except in compliance with the Licence.
+
+You may obtain a copy of the Licence at:
+https://gitext.gfz-potsdam.de/software/heesil
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the Licence is distributed on an "AS IS" basis,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the Licence for the specific language governing
+permissions and limitations under the Licence.
+-->
+<template>
+  <v-container>
+    <v-row>
+      <v-col cols="12" md="12">
+        <v-textarea
+          v-model="description"
+          label="Description"
+          rows="3"
+        />
+      </v-col>
+    </v-row>
+    <v-row>
+      <v-col cols="12" md="6">
+        <v-form
+          ref="contactForm"
+          v-model="contactIsValid"
+          @submit.prevent
+        >
+          <v-autocomplete
+            v-model="contact"
+            :items="contacts"
+            label="Contact"
+            clearable
+            required
+            :item-text="(x) => x.toString()"
+            :item-value="(x) => x"
+            :rules="rules"
+          />
+        </v-form>
+      </v-col>
+      <v-col cols="12" md="1" align-self="center">
+        <v-btn small @click="selectCurrentUserAsContact">
+          {{ labelForSelectMeButton }}
+        </v-btn>
+      </v-col>
+    </v-row>
+    <v-row>
+      <v-col>
+        <v-select
+          v-model="actionAttachments"
+          multiple
+          chips
+          clearable
+          deletable-chips
+          label="Attachments"
+          prepend-icon="mdi-paperclip"
+          no-data-text="There are no attachments for this device"
+          :items="attachments"
+          :item-text="(x) => x.label"
+          :item-value="(x) => x"
+          :disabled="!attachments.length"
+        />
+      </v-col>
+    </v-row>
+  </v-container>
+</template>
+
+<script lang="ts">
+/**
+ * @file provides a component for a set of common form fields for actions
+ * @author <marc.hanisch@gfz-potsdam.de>
+ */
+import { Component, Prop, Vue } from 'nuxt-property-decorator'
+
+import { Attachment } from '@/models/Attachment'
+import { Contact } from '@/models/Contact'
+import { GenericAction } from '@/models/GenericAction'
+
+/**
+ * A class component for a set of common form fields for actions
+ * @extends Vue
+ */
+@Component
+// @ts-ignore
+export default class CommonActionForm extends Vue {
+  private contacts: Contact[] = []
+  private readonly labelForSelectMeButton = 'Add current user'
+  private contactIsValid = true
+
+  /**
+   * a GenericAction
+   */
+  @Prop({
+    default: new GenericAction(),
+    required: true,
+    type: Object
+  })
+  // @ts-ignore
+  readonly value!: GenericAction
+
+  /**
+   * a list of available attachments
+   */
+  @Prop({
+    default: () => [],
+    required: false,
+    type: Array
+  })
+  // @ts-ignore
+  readonly attachments!: Attachment[]
+
+  /**
+   * rules
+   */
+  @Prop({
+    default: () => [],
+    required: false,
+    type: Array
+  })
+  // @ts-ignore
+  readonly rules!: ((v: any) => boolean | string)[]
+
+  async fetch () {
+    try {
+      this.contacts = await this.$api.contacts.findAll()
+    } catch (error) {
+      this.$store.commit('snackbar/setError', 'Failed to fetch contacts')
+    }
+  }
+
+  get description (): string {
+    return this.value.description
+  }
+
+  /**
+   * sets the new description
+   *
+   * @param {string} value - the description to set
+   * @fires CommonActionForm#input
+   */
+  set description (value: string) {
+    const actionCopy = GenericAction.createFromObject(this.value)
+    actionCopy.description = value
+    /**
+     * descriptionChange event
+     * @event CommonActionForm#input
+     * @type {string}
+     */
+    this.$emit('input', actionCopy)
+  }
+
+  get contact (): Contact | null {
+    return this.value.contact
+  }
+
+  /**
+   * sets the new contact
+   *
+   * @param {Contact | null} value - the contact to set
+   * @fires CommonActionForm#input
+   */
+  set contact (value: Contact | null) {
+    const actionCopy = GenericAction.createFromObject(this.value)
+    actionCopy.contact = value || null
+    /**
+     * contactChange event
+     * @event CommonActionForm#input
+     * @type {Contact}
+     */
+    this.$emit('input', actionCopy)
+  }
+
+  get actionAttachments (): Attachment[] {
+    return this.value.attachments
+  }
+
+  /**
+   * sets the new list of attachments
+   *
+   * @param {Attachment[]} value - the list of attachments to set
+   * @fires CommonActionForm#input
+   */
+  set actionAttachments (value: Attachment[]) {
+    const actionCopy = GenericAction.createFromObject(this.value)
+    actionCopy.attachments = value
+    /**
+     * attachments event
+     * @event CommonActionForm#input
+     * @type {Attachment[]}
+     */
+    this.$emit('input', actionCopy)
+  }
+
+  /**
+   * selects the current (loggined) user from the list of users and adds the
+   * user to the action
+   *
+   */
+  selectCurrentUserAsContact () {
+    const currentUserMail = this.$store.getters['oidc/userEmail']
+    if (currentUserMail) {
+      const userIndex = this.contacts.findIndex(c => c.email === currentUserMail)
+      if (userIndex > -1) {
+        this.contact = this.contacts[userIndex]
+        return
+      }
+    }
+    this.$store.commit('snackbar/setError', 'No contact found with your data')
+  }
+
+  /**
+   * returns whether the form is valid based on the defined rules
+   *
+   * @return {boolean} whether the form is valid or not
+   */
+  isValid (): boolean {
+    return (this.$refs.contactForm as Vue & { validate: () => boolean }).validate()
+  }
+}
+</script>
diff --git a/components/DatePicker.vue b/components/DatePicker.vue
index a423ab6d05e2c2dfb70ee7f5d998534a5de7032e..843b1d4dcaab876c651cb99e48f4e4b1e21f5269 100644
--- a/components/DatePicker.vue
+++ b/components/DatePicker.vue
@@ -39,7 +39,7 @@ permissions and limitations under the Licence.
       offset-y
       min-width="290px"
     >
-      <template v-slot:activator="{ on, attrs }">
+      <template #activator="{ on, attrs }">
         <v-text-field
           :value="getDate()"
           :rules="rules"
diff --git a/components/GenericActionCard.vue b/components/GenericActionCard.vue
new file mode 100644
index 0000000000000000000000000000000000000000..e895c12680d81e9dd00a87f89430581ef19fb895
--- /dev/null
+++ b/components/GenericActionCard.vue
@@ -0,0 +1,136 @@
+<!--
+Web client of the Sensor Management System software developed within the
+Helmholtz DataHub Initiative by GFZ and UFZ.
+
+Copyright (C) 2020, 2021
+- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+- Helmholtz Centre Potsdam - GFZ German Research Centre for
+  Geosciences (GFZ, https://www.gfz-potsdam.de)
+
+Parts of this program were developed within the context of the
+following publicly funded projects or measures:
+- Helmholtz Earth and Environment DataHub
+  (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+
+Licensed under the HEESIL, Version 1.0 or - as soon they will be
+approved by the "Community" - subsequent versions of the HEESIL
+(the "Licence").
+
+You may not use this work except in compliance with the Licence.
+
+You may obtain a copy of the Licence at:
+https://gitext.gfz-potsdam.de/software/heesil
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the Licence is distributed on an "AS IS" basis,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the Licence for the specific language governing
+permissions and limitations under the Licence.
+-->
+<template>
+  <v-card>
+    <v-card-subtitle class="pb-0">
+      <v-row no-gutters>
+        <v-col>
+          {{ value.beginDate | toUtcDate }} - {{ value.endDate | toUtcDate }}
+        </v-col>
+        <v-col
+          align-self="end"
+          class="text-right"
+        >
+          <slot name="menu" />
+        </v-col>
+      </v-row>
+    </v-card-subtitle>
+    <v-card-title class="pt-0">
+      {{ value.actionTypeName }}
+    </v-card-title>
+    <v-card-subtitle class="pb-1">
+      <v-row
+        no-gutters
+      >
+        <v-col>
+          {{ value.contact.toString() }}
+        </v-col>
+        <v-col
+          align-self="end"
+          class="text-right"
+        >
+          <slot name="actions" />
+          <v-btn
+            icon
+            @click.stop.prevent="toggleVisibility()"
+          >
+            <v-icon>{{ isVisible() ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
+          </v-btn>
+        </v-col>
+      </v-row>
+    </v-card-subtitle>
+    <v-expand-transition>
+      <div
+        v-show="isVisible(value.id)"
+      >
+        <v-card-text
+          class="grey lighten-5 text--primary pt-2"
+        >
+          <label>Description</label>
+          {{ value.description }}
+        </v-card-text>
+      </div>
+    </v-expand-transition>
+  </v-card>
+</template>
+
+<script lang="ts">
+/**
+ * @file provides a component for a Generic Device Actions card
+ * @author <marc.hanisch@gfz-potsdam.de>
+ */
+import { Component, Prop, Vue } from 'nuxt-property-decorator'
+
+import { dateToDateTimeString } from '@/utils/dateHelper'
+import { GenericAction } from '@/models/GenericAction'
+
+/**
+ * A class component for Generic Device Action card
+ * @extends Vue
+ */
+@Component({
+  filters: {
+    toUtcDate: dateToDateTimeString
+  }
+})
+// @ts-ignore
+export default class GenericActionCard extends Vue {
+  private showDetails: boolean = false
+
+  /**
+   * a GenericAction
+   */
+  @Prop({
+    default: () => new GenericAction(),
+    required: true,
+    type: Object
+  })
+  // @ts-ignore
+  readonly value!: GenericAction
+
+  /**
+   * whether the card expansion is shown or not
+   *
+   * @return {boolean} whether the card expansion is shown or not
+   */
+  isVisible (): boolean {
+    return this.showDetails
+  }
+
+  /**
+   * toggles the shown state of the card expansion
+   *
+   */
+  toggleVisibility (): void {
+    this.showDetails = !this.showDetails
+  }
+}
+</script>
diff --git a/components/GenericActionForm.vue b/components/GenericActionForm.vue
new file mode 100644
index 0000000000000000000000000000000000000000..3973e7cce4df676ff4d91ab20ef9ac8506c919ae
--- /dev/null
+++ b/components/GenericActionForm.vue
@@ -0,0 +1,248 @@
+<!--
+Web client of the Sensor Management System software developed within the
+Helmholtz DataHub Initiative by GFZ and UFZ.
+
+Copyright (C) 2020, 2021
+- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+- Helmholtz Centre Potsdam - GFZ German Research Centre for
+  Geosciences (GFZ, https://www.gfz-potsdam.de)
+
+Parts of this program were developed within the context of the
+following publicly funded projects or measures:
+- Helmholtz Earth and Environment DataHub
+  (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+
+Licensed under the HEESIL, Version 1.0 or - as soon they will be
+approved by the "Community" - subsequent versions of the HEESIL
+(the "Licence").
+
+You may not use this work except in compliance with the Licence.
+
+You may obtain a copy of the Licence at:
+https://gitext.gfz-potsdam.de/software/heesil
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the Licence is distributed on an "AS IS" basis,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the Licence for the specific language governing
+permissions and limitations under the Licence.
+-->
+<template>
+  <v-container>
+    <v-form
+      ref="datesForm"
+      v-model="datesAreValid"
+      @submit.prevent
+    >
+      <v-row>
+        <v-col cols="12" md="6">
+          <DatePicker
+            :value="actionCopy.beginDate"
+            label="Start date"
+            :rules="[rules.startDate, rules.startDateNotNull]"
+            @input="setStartDateAndValidate"
+          />
+        </v-col>
+        <v-col cols="12" md="6">
+          <DatePicker
+            :value="actionCopy.endDate"
+            label="End date"
+            :rules="[rules.endDate]"
+            @input="setEndDateAndValidate"
+          />
+        </v-col>
+      </v-row>
+    </v-form>
+    <CommonActionForm
+      ref="commonForm"
+      v-model="action"
+      :attachments="attachments"
+      :rules="[rules.contactNotNull]"
+    />
+  </v-container>
+</template>
+
+<script lang="ts">
+/**
+ * @file provides a component for a Generic Device Actions form
+ * @author <marc.hanisch@gfz-potsdam.de>
+ */
+import { Component, Prop, Vue, Watch } from 'nuxt-property-decorator'
+
+import { DateTime } from 'luxon'
+import { stringToDate } from '@/utils/dateHelper'
+
+import { Attachment } from '@/models/Attachment'
+import { GenericAction } from '@/models/GenericAction'
+
+import CommonActionForm from '@/components/CommonActionForm.vue'
+import DatePicker from '@/components/DatePicker.vue'
+
+/**
+ * A class component for a form for Generic Device Actions
+ * @extends Vue
+ */
+@Component({
+  components: {
+    CommonActionForm,
+    DatePicker
+  }
+})
+// @ts-ignore
+export default class GenericActionForm extends Vue {
+  private actionCopy: GenericAction = new GenericAction()
+  private datesAreValid: boolean = true
+  private rules: Object = {
+    startDate: this.validateInputForStartDate,
+    startDateNotNull: this.mustBeProvided('Start date'),
+    endDate: this.validateInputForEndDate,
+    contactNotNull: this.mustBeProvided('Contact')
+  }
+
+  /**
+   * a GenericAction
+   */
+  @Prop({
+    default: () => new GenericAction(),
+    required: true,
+    type: Object
+  })
+  // @ts-ignore
+  readonly value!: GenericAction
+
+  /**
+   * a list of available attachments
+   */
+  @Prop({
+    default: () => [],
+    required: false,
+    type: Array
+  })
+  // @ts-ignore
+  readonly attachments!: Attachment[]
+
+  created () {
+    // create a copy of the original value on which all operations will be applied
+    this.createActionCopy(this.value)
+  }
+
+  get action (): GenericAction {
+    return this.actionCopy
+  }
+
+  set action (value: GenericAction) {
+    this.$emit('input', value)
+  }
+
+  /**
+   * sets the start date and validates start- and enddate
+   *
+   * @param {DateTime | null} aDate - the start date
+   */
+  setStartDateAndValidate (aDate: DateTime | null) {
+    this.actionCopy.beginDate = aDate
+    if (this.actionCopy.endDate !== null) {
+      this.checkValidationOfDates()
+    }
+    this.$emit('input', this.actionCopy)
+  }
+
+  /**
+   * sets the end date and validates start- and enddate
+   *
+   * @param {DateTime | null} aDate - the end date
+   */
+  setEndDateAndValidate (aDate: DateTime | null) {
+    this.actionCopy.endDate = aDate
+    if (this.actionCopy.beginDate !== null) {
+      this.checkValidationOfDates()
+    }
+    this.$emit('input', this.actionCopy)
+  }
+
+  /**
+   * validates the form based on its rules
+   *
+   */
+  checkValidationOfDates () {
+    return (this.$refs.datesForm as Vue & { validate: () => boolean }).validate()
+  }
+
+  /**
+   * a rule to validate the start date
+   *
+   * @param {string} v - a string representation of the date as supplied by the datepicker component
+   * @return {boolean | string} whether the date is valid or an error message
+   */
+  validateInputForStartDate (v: string): boolean | string {
+    // NOTE: as the internals of the DatePicker component work with strings,
+    // the validation functions should expect strings, too
+    if (v === null || v === '') {
+      return true
+    }
+    if (!this.actionCopy.endDate) {
+      return true
+    }
+    if (stringToDate(v) <= this.actionCopy.endDate) {
+      return true
+    }
+    return 'Start date must not be after end date'
+  }
+
+  /**
+   * a rule to validate the end date
+   *
+   * @param {string} v - a string representation of the date as supplied by the datepicker component
+   * @return {boolean | string} whether the date is valid or an error message
+   */
+  validateInputForEndDate (v: string): boolean | string {
+    // NOTE: as the internals of the DatePicker component work with strings,
+    // the validation functions should expect strings, too
+    if (v === null || v === '') {
+      return true
+    }
+    if (!this.actionCopy.beginDate) {
+      return true
+    }
+    if (stringToDate(v) >= this.actionCopy.beginDate) {
+      return true
+    }
+    return 'End date must not be before start date'
+  }
+
+  /**
+   * a rule to check that an field is non-empty
+   *
+   * @param {string} fieldname - the (human readable) label of the field
+   * @return {(v: any) => boolean | string} a function that checks whether the field is valid or an error message
+   */
+  mustBeProvided (fieldname: string): (v: any) => boolean | string {
+    const innerFunc: (v: any) => boolean | string = function (v: any) {
+      if (v == null || v === '') {
+        return fieldname + ' must be provided'
+      }
+      return true
+    }
+    return innerFunc
+  }
+
+  /**
+   * checks if the form is valid
+   *
+   */
+  isValid (): boolean {
+    return this.checkValidationOfDates() && (this.$refs.commonForm as Vue & { isValid: () => boolean }).isValid()
+  }
+
+  createActionCopy (action: GenericAction): void {
+    this.actionCopy = GenericAction.createFromObject(action)
+  }
+
+  @Watch('value', { immediate: true, deep: true })
+  // @ts-ignore
+  onValueChanged (val: GenericAction) {
+    this.createActionCopy(val)
+  }
+}
+</script>
diff --git a/modelUtils/Compareables.ts b/modelUtils/Compareables.ts
new file mode 100644
index 0000000000000000000000000000000000000000..bbd815b9ab2209127103ec9587adc93525bd302a
--- /dev/null
+++ b/modelUtils/Compareables.ts
@@ -0,0 +1,31 @@
+import { DateTime } from 'luxon'
+
+export interface IComparator<T> {
+  compare (a: T, b: T): number
+}
+
+export interface IDateCompareable {
+  date: DateTime | null
+}
+
+export function isDateCompareable (i: any): i is IDateCompareable {
+  if (i && typeof i === 'object' && Object.prototype.hasOwnProperty.call(i, 'date') && DateTime.isDateTime(i.date)) {
+    return true
+  }
+  return false
+}
+
+export class DateComparator implements IComparator<IDateCompareable> {
+  compare (a: IDateCompareable, b: IDateCompareable): number {
+    if (!a.date && !b.date) {
+      return 0
+    }
+    if (!a.date || (b.date && a.date < b.date)) {
+      return -1
+    }
+    if (!b.date || (a.date && a.date > b.date)) {
+      return 1
+    }
+    return 0
+  }
+}
diff --git a/models/Action.ts b/models/Action.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4d90f32857ac2ac4a11f33edb5331d48c5c42de6
--- /dev/null
+++ b/models/Action.ts
@@ -0,0 +1,38 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { Contact } from '@/models/Contact'
+
+export interface IAction {
+  id: string | null
+  description: string
+  contact: Contact | null
+}
diff --git a/models/ActionType.ts b/models/ActionType.ts
new file mode 100644
index 0000000000000000000000000000000000000000..ce53694850b0e9fee426f8b898cc909f46da9354
--- /dev/null
+++ b/models/ActionType.ts
@@ -0,0 +1,100 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+export interface IActionType {
+  id: string
+  name: string
+  uri: string
+  definition: string
+}
+
+export class ActionType implements IActionType {
+  private _id: string = ''
+  private _name: string = ''
+  private _uri: string = ''
+  private _definition: string = ''
+
+  get id (): string {
+    return this._id
+  }
+
+  set id (newId: string) {
+    this._id = newId
+  }
+
+  get name (): string {
+    return this._name
+  }
+
+  set name (newName: string) {
+    this._name = newName
+  }
+
+  get uri (): string {
+    return this._uri
+  }
+
+  set uri (newUri: string) {
+    this._uri = newUri
+  }
+
+  get definition (): string {
+    return this._definition
+  }
+
+  set definition (newdefinition: string) {
+    this._definition = newdefinition
+  }
+
+  toString (): string {
+    return this._name
+  }
+
+  static createWithData (id: string, name: string, uri: string, definition: string): ActionType {
+    const result = new ActionType()
+    result.id = id
+    result.name = name
+    result.uri = uri
+    result.definition = definition
+    return result
+  }
+
+  static createFromObject (someObject: IActionType): ActionType {
+    const newObject = new ActionType()
+
+    newObject.id = someObject.id
+    newObject.name = someObject.name
+    newObject.uri = someObject.uri
+    newObject.definition = someObject.definition
+
+    return newObject
+  }
+}
diff --git a/models/GenericAction.ts b/models/GenericAction.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4d144df1fa2bf1e8c0560974cdf92adc2c1074fe
--- /dev/null
+++ b/models/GenericAction.ts
@@ -0,0 +1,158 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { DateTime } from 'luxon'
+import { Attachment } from '@/models/Attachment'
+import { Contact } from '@/models/Contact'
+import { IAction } from '@/models/Action'
+import { IDateCompareable } from '@/modelUtils/Compareables'
+
+export interface IGenericAction extends IAction {
+  actionTypeName: string
+  actionTypeUrl: string
+  beginDate: DateTime | null
+  endDate: DateTime | null
+  contact: Contact | null
+  attachments: Attachment[]
+}
+
+export class GenericAction implements IGenericAction, IDateCompareable {
+  private _id: string | null = null
+  private _description: string = ''
+  private _actionTypeName: string = ''
+  private _actionTypeUrl: string = ''
+  private _beginDate: DateTime | null = null
+  private _endDate: DateTime | null = null
+  private _contact: Contact | null = null
+  private _attachments: Attachment[] = []
+
+  /**
+   * returns an empty instance
+   *
+   * @static
+   * @return {GenericAction} an empty instance
+   */
+  static createEmpty (): GenericAction {
+    return new GenericAction()
+  }
+
+  /**
+   * creates an instance from an existing IGenericAction-like object
+   *
+   * @static
+   * @param {IGenericAction} someObject - an IGenericAction like object
+   * @return {GenericAction} a cloned instance of the original object
+   */
+  static createFromObject (someObject: IGenericAction) : GenericAction {
+    const action = new GenericAction()
+    action.id = someObject.id
+    action.description = someObject.description
+    action.actionTypeName = someObject.actionTypeName
+    action.actionTypeUrl = someObject.actionTypeUrl
+    action.beginDate = someObject.beginDate ? someObject.beginDate : null
+    action.endDate = someObject.endDate ? someObject.endDate : null
+    action.contact = someObject.contact ? Contact.createFromObject(someObject.contact) : null
+    action.attachments = someObject.attachments.map(i => Attachment.createFromObject(i))
+    return action
+  }
+
+  get id (): string | null {
+    return this._id
+  }
+
+  set id (id: string | null) {
+    this._id = id
+  }
+
+  get description (): string {
+    return this._description
+  }
+
+  set description (description: string) {
+    this._description = description
+  }
+
+  get actionTypeUrl (): string {
+    return this._actionTypeUrl
+  }
+
+  set actionTypeUrl (actionTypeUrl: string) {
+    this._actionTypeUrl = actionTypeUrl
+  }
+
+  get actionTypeName (): string {
+    return this._actionTypeName
+  }
+
+  set actionTypeName (actionTypeName: string) {
+    this._actionTypeName = actionTypeName
+  }
+
+  get beginDate (): DateTime | null {
+    return this._beginDate
+  }
+
+  set beginDate (date: DateTime | null) {
+    this._beginDate = date
+  }
+
+  get endDate (): DateTime | null {
+    return this._endDate
+  }
+
+  set endDate (date: DateTime | null) {
+    this._endDate = date
+  }
+
+  get contact (): Contact | null {
+    return this._contact
+  }
+
+  set contact (contact: Contact | null) {
+    this._contact = contact
+  }
+
+  get attachments (): Attachment[] {
+    return this._attachments
+  }
+
+  set attachments (attachments: Attachment[]) {
+    this._attachments = attachments
+  }
+
+  get isGenericAction (): boolean {
+    return true
+  }
+
+  get date (): DateTime | null {
+    return this.beginDate
+  }
+}
diff --git a/pages/devices/_deviceId/actions.vue b/pages/devices/_deviceId/actions.vue
index 614eefe9b58e984feeec2dccae18417c55f94741..da0005ceaac806a893b78490893989b3b9b257a5 100644
--- a/pages/devices/_deviceId/actions.vue
+++ b/pages/devices/_deviceId/actions.vue
@@ -37,74 +37,107 @@ permissions and limitations under the Licence.
     <v-card-actions>
       <v-spacer />
       <v-btn
-        v-if="isLoggedIn && !(isAddActionPage)"
+        v-if="isLoggedIn && !(isAddActionPage || isEditActionPage)"
         color="primary"
         small
-        :disabled="isEditActionPage"
         :to="'/devices/' + deviceId + '/actions/new'"
       >
         Add Action
       </v-btn>
     </v-card-actions>
-    <template v-if="isAddActionPage">
-      <NuxtChild />
+    <template
+      v-if="isAddActionPage"
+    >
+      <NuxtChild
+        @input="$fetch"
+        @showsave="showsave"
+      />
+    </template>
+    <template
+      v-else-if="isEditActionPage"
+    >
+      <!-- the currently edited action is passed as a property to the
+           `edit.vue` page to avoid reloading of the action -->
+      <NuxtChild
+        :value="editedAction"
+        @input="$fetch"
+        @showsave="showsave"
+      />
     </template>
     <template v-else>
       <v-timeline dense>
         <v-timeline-item
-          v-for="action in actions"
-          :key="action.getId()"
+          v-for="(action, index) in actions"
+          :key="getActionTypeIterationKey(action)"
           :color="action.getColor()"
           class="mb-4"
           small
         >
-          <template v-if="action.isGenericDeviceAction">
-            <v-card>
-              <v-card-subtitle class="pb-0">
-                {{ action.beginDate | toUtcDate }} - {{ action.endDate | toUtcDate }}
-              </v-card-subtitle>
-              <v-card-title class="py-0">
-                <v-row
-                  no-gutters
-                >
-                  <v-col
-                    cols="11"
+          <GenericActionCard
+            v-if="action.isGenericAction"
+            v-model="actions[index]"
+          >
+            <template #menu>
+              <v-menu
+                close-on-click
+                close-on-content-click
+                offset-x
+                left
+                z-index="999"
+              >
+                <template #activator="{ on }">
+                  <v-btn
+                    data-role="property-menu"
+                    icon
+                    small
+                    v-on="on"
                   >
-                    {{ action.actionTypeName }}
-                  </v-col>
-                  <v-col
-                    align-self="end"
-                    class="text-right"
-                  >
-                    <v-btn
-                      icon
-                      @click.stop.prevent="showActionItem(action.getId())"
+                    <v-icon
+                      dense
+                      small
                     >
-                      <v-icon>{{ isActionItemShown(action.getId()) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
-                    </v-btn>
-                  </v-col>
-                </v-row>
-              </v-card-title>
-              <v-expand-transition>
-                <div
-                  v-show="isActionItemShown(action.getId())"
-                >
-                  <v-card-subtitle
-                    class="pt-0"
+                      mdi-dots-vertical
+                    </v-icon>
+                  </v-btn>
+                </template>
+
+                <v-list>
+                  <v-list-item
+                    :disabled="!isLoggedIn"
+                    dense
+                    @click="showDeleteDialog(action.id)"
                   >
-                    {{ action.contact.toString() }}
-                  </v-card-subtitle>
-                  <v-card-text
-                    class="grey lighten-5 text--primary pt-2"
-                  >
-                    <label>Description</label>
-                    {{ action.description }}
-                  </v-card-text>
-                </div>
-              </v-expand-transition>
-            </v-card>
-          </template>
-          <template v-if="action.isUpdateAction">
+                    <v-list-item-content>
+                      <v-list-item-title
+                        :class="isLoggedIn ? 'red--text' : 'grey--text'"
+                      >
+                        <v-icon
+                          left
+                          small
+                          :color="isLoggedIn ? 'red' : 'grey'"
+                        >
+                          mdi-delete
+                        </v-icon>
+                        Delete
+                      </v-list-item-title>
+                    </v-list-item-content>
+                  </v-list-item>
+                </v-list>
+              </v-menu>
+            </template>
+            <template #actions>
+              <v-btn
+                :to="'/devices/' + deviceId + '/actions/' + action.id + '/edit'"
+                color="primary"
+                text
+                @click.stop.prevent
+              >
+                Edit
+              </v-btn>
+            </template>
+          </GenericActionCard>
+
+          <template v-if="action.isDeviceSoftwareUpdateAction">
             <v-card>
               <v-card-subtitle class="pb-0">
                 {{ action.updateDate | toUtcDate }}
@@ -127,16 +160,16 @@ permissions and limitations under the Licence.
                   >
                     <v-btn
                       icon
-                      @click.stop.prevent="showActionItem(action.getId())"
+                      @click.stop.prevent="showActionItem(action.id)"
                     >
-                      <v-icon>{{ isActionItemShown(action.getId()) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
+                      <v-icon>{{ isActionItemShown(action.id) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
                     </v-btn>
                   </v-col>
                 </v-row>
               </v-card-subtitle>
               <v-expand-transition>
                 <v-card-text
-                  v-show="isActionItemShown(action.getId())"
+                  v-show="isActionItemShown(action.id)"
                   class="text--primary"
                 >
                   <v-row dense>
@@ -186,16 +219,16 @@ permissions and limitations under the Licence.
                   >
                     <v-btn
                       icon
-                      @click.stop.prevent="showActionItem(action.getId())"
+                      @click.stop.prevent="showActionItem(action.id)"
                     >
-                      <v-icon>{{ isActionItemShown(action.getId()) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
+                      <v-icon>{{ isActionItemShown(action.id) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
                     </v-btn>
                   </v-col>
                 </v-row>
               </v-card-subtitle>
               <v-expand-transition>
                 <v-card-text
-                  v-show="isActionItemShown(action.getId())"
+                  v-show="isActionItemShown(action.id)"
                   class="text--primary"
                 >
                   <v-row dense>
@@ -247,16 +280,16 @@ permissions and limitations under the Licence.
                   >
                     <v-btn
                       icon
-                      @click.stop.prevent="showActionItem(action.getId())"
+                      @click.stop.prevent="showActionItem(action.id)"
                     >
-                      <v-icon>{{ isActionItemShown(action.getId()) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
+                      <v-icon>{{ isActionItemShown(action.id) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
                     </v-btn>
                   </v-col>
                 </v-row>
               </v-card-subtitle>
               <v-expand-transition>
                 <v-card-text
-                  v-show="isActionItemShown(action.getId())"
+                  v-show="isActionItemShown(action.id)"
                   class="text--primary"
                 >
                   <label>Parent platform</label>{{ action.parentPlatformName }}
@@ -298,16 +331,16 @@ permissions and limitations under the Licence.
                   >
                     <v-btn
                       icon
-                      @click.stop.prevent="showActionItem(action.getId())"
+                      @click.stop.prevent="showActionItem(action.id)"
                     >
-                      <v-icon>{{ isActionItemShown(action.getId()) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
+                      <v-icon>{{ isActionItemShown(action.id) ? 'mdi-chevron-up' : 'mdi-chevron-down' }}</v-icon>
                     </v-btn>
                   </v-col>
                 </v-row>
               </v-card-subtitle>
               <v-expand-transition>
                 <v-card-text
-                  v-show="isActionItemShown(action.getId())"
+                  v-show="isActionItemShown(action.id)"
                   class="text--primary"
                 />
               </v-expand-transition>
@@ -315,79 +348,68 @@ permissions and limitations under the Licence.
           </template>
         </v-timeline-item>
       </v-timeline>
+      <v-dialog v-model="hasActionIdToDelete" max-width="290">
+        <v-card>
+          <v-card-title class="headline">
+            Delete action
+          </v-card-title>
+          <v-card-text>
+            Do you really want to delete the action?
+          </v-card-text>
+          <v-card-actions>
+            <v-btn
+              text
+              @click="hideDeleteDialog()"
+            >
+              No
+            </v-btn>
+            <v-spacer />
+            <v-btn
+              color="error"
+              text
+              @click="deleteActionAndCloseDialog(actionIdToDelete)"
+            >
+              <v-icon left>
+                mdi-delete
+              </v-icon>
+              Delete
+            </v-btn>
+          </v-card-actions>
+        </v-card>
+      </v-dialog>
     </template>
   </div>
 </template>
-<style lang="scss">
-@import "@/assets/styles/_forms.scss";
-</style>
 <script lang="ts">
 import { Component, Vue } from 'nuxt-property-decorator'
 import ProgressIndicator from '@/components/ProgressIndicator.vue'
+import GenericActionCard from '@/components/GenericActionCard.vue'
 
 import { DateTime } from 'luxon'
 
 import { Contact } from '@/models/Contact'
 import { DeviceProperty } from '@/models/DeviceProperty'
+import { IAction } from '@/models/Action'
+import { GenericAction } from '@/models/GenericAction'
+import { DateComparator, isDateCompareable } from '@/modelUtils/Compareables'
 
 const toUtcDate = (dt: DateTime) => {
   return dt.toUTC().toFormat('yyyy-MM-dd TT')
 }
 
-interface IAction {
-  getId (): string
+interface IColoredAction {
   getColor (): string
 }
 
-class GenericDeviceAction implements IAction {
-  private id: string
-  private description: string
-  private beginDate: DateTime
-  private endDate: DateTime
-  private actionTypeName: string
-  private actionTypeUri: string
-  private contact: Contact
-
-  constructor (
-    id: string,
-    description: string,
-    beginDate: DateTime,
-    endDate: DateTime,
-    actionTypeName: string,
-    actionTypeUri: string,
-    contact: Contact
-  ) {
-    this.id = id
-    this.description = description
-    this.beginDate = beginDate
-    this.endDate = endDate
-    this.actionTypeName = actionTypeName
-    this.actionTypeUri = actionTypeUri
-    this.contact = contact
-  }
-
-  getId (): string {
-    return 'generic-' + this.id
-  }
-
-  getColor (): string {
-    return 'blue'
-  }
-
-  get isGenericDeviceAction (): boolean {
-    return true
-  }
-}
-
-class DeviceSoftwareUpdateAction implements IAction {
-  private id: string
-  private softwareTypeName: string
-  private softwareTypeUri: string
-  private updateDate: DateTime
-  private version: string
-  private repositoryUrl: string
-  private description: string
-  private contact: Contact
+class DeviceSoftwareUpdateAction implements IAction, IColoredAction {
+  public id: string
+  public softwareTypeName: string
+  public softwareTypeUri: string
+  public updateDate: DateTime
+  public version: string
+  public repositoryUrl: string
+  public description: string
+  public contact: Contact
   constructor (
     id: string,
     softwareTypeName: string,
@@ -412,24 +434,24 @@ class DeviceSoftwareUpdateAction implements IAction {
     return 'software-' + this.id
   }
 
-  getColor (): string {
-    return 'yellow'
+  get isDeviceSoftwareUpdateAction (): boolean {
+    return true
   }
 
-  get isUpdateAction (): boolean {
-    return true
+  getColor (): string {
+    return 'yellow'
   }
 }
 
-class DeviceCalibrationAction implements IAction {
-  private id: string
-  private description: string
-  private currentCalibrationDate: DateTime
-  private nextCalibrationDate: DateTime
-  private formula: string
-  private value: string
-  private deviceProperties: DeviceProperty[]
-  private contact: Contact
+class DeviceCalibrationAction implements IAction, IColoredAction {
+  public id: string
+  public description: string
+  public currentCalibrationDate: DateTime
+  public nextCalibrationDate: DateTime
+  public formula: string
+  public value: string
+  public deviceProperties: DeviceProperty[]
+  public contact: Contact
   constructor (
     id: string,
     description: string,
@@ -454,24 +476,25 @@ class DeviceCalibrationAction implements IAction {
     return 'calibration-' + this.id
   }
 
-  getColor (): string {
-    return 'brown'
-  }
-
   get isDeviceCalibrationAction (): boolean {
     return true
   }
+
+  getColor (): string {
+    return 'brown'
+  }
 }
 
 class DeviceMountAction {
-  private id: string
-  private configurationName: string
-  private parentPlatformName: string
-  private offsetX: number
-  private offsetY: number
-  private offsetZ: number
-  private beginDate: DateTime
-  private contact: Contact
+  public id: string
+  public configurationName: string
+  public parentPlatformName: string
+  public offsetX: number
+  public offsetY: number
+  public offsetZ: number
+  public beginDate: DateTime
+  public description: string
+  public contact: Contact
   constructor (
     id: string,
     configurationName: string,
@@ -480,6 +503,7 @@ class DeviceMountAction {
     offsetY: number,
     offsetZ: number,
     beginDate: DateTime,
+    description: string,
     contact: Contact
   ) {
     this.id = id
@@ -489,6 +513,7 @@ class DeviceMountAction {
     this.offsetY = offsetY
     this.offsetZ = offsetZ
     this.beginDate = beginDate
+    this.description = description
     this.contact = contact
   }
 
@@ -496,29 +521,32 @@ class DeviceMountAction {
     return 'mount-' + this.id
   }
 
-  getColor (): string {
-    return 'green'
-  }
-
   get isDeviceMountAction (): boolean {
     return true
   }
+
+  getColor (): string {
+    return 'green'
+  }
 }
 
-class DeviceUnmountAction implements IAction {
-  private id: string
-  private configurationName: string
-  private endDate: DateTime
-  private contact: Contact
+class DeviceUnmountAction implements IAction, IColoredAction {
+  public id: string
+  public configurationName: string
+  public endDate: DateTime
+  public description: string
+  public contact: Contact
   constructor (
     id: string,
     configurationName: string,
     endDate: DateTime,
+    description: string,
     contact: Contact
   ) {
     this.id = id
     this.configurationName = configurationName
     this.endDate = endDate
+    this.description = description
     this.contact = contact
   }
 
@@ -526,18 +554,28 @@ class DeviceUnmountAction implements IAction {
     return 'unmount-' + this.id
   }
 
+  get isDeviceUnmountAction (): boolean {
+    return true
+  }
+
   getColor (): string {
     return 'red'
   }
+}
 
-  get isDeviceUnmountAction (): boolean {
-    return true
+/**
+ * extend the original interface by adding the getColor() method
+ */
+declare module '@/models/GenericAction' {
+  export interface GenericAction extends IColoredAction {
   }
 }
+GenericAction.prototype.getColor = (): string => 'blue'
 
 @Component({
   components: {
-    ProgressIndicator
+    ProgressIndicator,
+    GenericActionCard
   },
   filters: {
     toUtcDate
@@ -550,32 +588,25 @@ export default class DeviceActionsPage extends Vue {
   private actions: IAction[] = []
   private searchResultItemsShown: { [id: string]: boolean } = {}
 
-  /* async */ fetch () {
+  private actionIdToDelete: string = ''
+
+  async fetch () {
     const contact1 = Contact.createFromObject({
-      id: '1',
+      id: 'X1',
       givenName: 'Tech',
       familyName: 'Niker',
       email: 'tech.niker@gfz-potsdam.de',
       website: ''
     })
     const contact2 = Contact.createFromObject({
-      id: '2',
+      id: 'X2',
       givenName: 'Cam',
       familyName: 'Paign',
       email: 'cam.paign@gfz-potsdam.de',
       website: ''
     })
-    const genericDeviceAction1 = new GenericDeviceAction(
-      '1',
-      'Grass cut on the site',
-      DateTime.fromISO('2021-03-29T08:00:00.000Z'),
-      DateTime.fromISO('2021-03-29T08:30:00Z'),
-      'Device visit',
-      'actionTypes/device_visit',
-      contact1
-    )
     const deviceSoftwareUpdateAction = new DeviceSoftwareUpdateAction(
-      '2',
+      'X2',
       'Firmware',
       'softwaretypes/firmware',
       DateTime.fromISO('2021-03-30T08:10:00Z'),
@@ -589,7 +620,7 @@ export default class DeviceActionsPage extends Vue {
     const devProp2 = new DeviceProperty()
     devProp2.label = 'Wind speed'
     const deviceCalibrationAction1 = new DeviceCalibrationAction(
-      '3',
+      'X3',
       'Calibration of the device for usage on the campaign',
       DateTime.fromISO('2021-03-30T08:12:00Z'),
       DateTime.fromISO('2021-04-30T12:00:00Z'),
@@ -599,28 +630,51 @@ export default class DeviceActionsPage extends Vue {
       contact2
     )
     const deviceMountAction1 = new DeviceMountAction(
-      '4',
+      'X4',
       'Measurement ABC',
       'Station ABC',
       0,
       0,
       2,
       DateTime.fromISO('2021-03-30T12:00:00Z'),
+      'Mounted Measurement ABC',
       contact1
     )
     const deviceUnmountAction1 = new DeviceUnmountAction(
-      '5',
+      'X5',
       'Measurement ABC',
       DateTime.fromISO('2022-03-30T12:00:00Z'),
+      'Unmounted Measurement ABC',
       contact1
     )
     this.actions = [
-      genericDeviceAction1,
       deviceSoftwareUpdateAction,
       deviceCalibrationAction1,
       deviceMountAction1,
       deviceUnmountAction1
     ]
+    await Promise.all([this.fetchGenericActions()])
+
+    // sort the actions
+    const comparator = new DateComparator()
+    this.actions.sort((i: IAction, j: IAction): number => {
+      if (isDateCompareable(i) && isDateCompareable(j)) {
+        // multiply result with -1 to get descending order
+        return comparator.compare(i, j) * -1
+      }
+      if (isDateCompareable(i)) {
+        return 1
+      }
+      if (isDateCompareable(j)) {
+        return -1
+      }
+      return 0
+    })
+  }
+
+  async fetchGenericActions (): Promise<void> {
+    const actions: GenericAction[] = await this.$api.devices.findRelatedGenericActions(this.deviceId)
+    actions.forEach((action: GenericAction) => this.actions.push(action))
   }
 
   get isInProgress (): boolean {
@@ -659,5 +713,79 @@ export default class DeviceActionsPage extends Vue {
   get deviceId (): string {
     return this.$route.params.deviceId
   }
+
+  get actionId (): string | undefined {
+    return this.$route.params.actionId
+  }
+
+  /**
+   * Returns the action object from the list of actions that is currently edited
+   *
+   * When the `/edit` route for an action is called, the `edit.vue` page is
+   * included via a `NuxtChild` component. To avoid of loading the action again
+   * in this page, we return it from this method to pass it as a property to
+   * the `NuxtChild` component
+   *
+   * Calls {@link DeviceActionsPage.actionId} to get the currently edited
+   * action from the route.
+   *
+   * @return {IAction | undefined} the found action, otherwise undefined
+   */
+  get editedAction (): IAction | undefined {
+    if (!this.actionId) {
+      return
+    }
+    return this.actions.find(action => action.id === this.actionId)
+  }
+
+  get hasActionIdToDelete (): boolean {
+    return !!this.actionIdToDelete
+  }
+
+  showsave (isSaving: boolean) {
+    this.isSaving = isSaving
+  }
+
+  showDeleteDialog (id: string) {
+    this.actionIdToDelete = id
+  }
+
+  hideDeleteDialog (): void {
+    this.actionIdToDelete = ''
+  }
+
+  deleteActionAndCloseDialog (id: string) {
+    this.isSaving = true
+    this.$api.genericDeviceActions.deleteById(id).then(() => {
+      this.$fetch()
+      this.$store.commit('snackbar/setSuccess', 'Action deleted')
+    }).catch((_error) => {
+      this.$store.commit('snackbar/setError', 'Action could not be deleted')
+    }).finally(() => {
+      this.hideDeleteDialog()
+      this.isSaving = false
+    })
+  }
+
+  getActionType (action: IAction): string {
+    switch (true) {
+      case 'isGenericAction' in action:
+        return 'generic-action'
+      case 'isDeviceSoftwareUpdateAction' in action:
+        return 'software-update-action'
+      case 'isDeviceCalibrationAction' in action:
+        return 'device-calibration-action'
+      default:
+        return 'unknown-action'
+    }
+  }
+
+  getActionTypeIterationKey (action: IAction): string {
+    return this.getActionType(action) + '-' + action.id
+  }
 }
 </script>
+
+<style lang="scss">
+@import "@/assets/styles/_forms.scss";
+</style>
diff --git a/pages/devices/_deviceId/actions/_actionId/edit.vue b/pages/devices/_deviceId/actions/_actionId/edit.vue
new file mode 100644
index 0000000000000000000000000000000000000000..52eda9234c14fbb53b215299ce25df719d992deb
--- /dev/null
+++ b/pages/devices/_deviceId/actions/_actionId/edit.vue
@@ -0,0 +1,168 @@
+<!--
+Web client of the Sensor Management System software developed within the
+Helmholtz DataHub Initiative by GFZ and UFZ.
+
+Copyright (C) 2020, 2021
+- Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+- Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+- Helmholtz Centre Potsdam - GFZ German Research Centre for
+  Geosciences (GFZ, https://www.gfz-potsdam.de)
+
+Parts of this program were developed within the context of the
+following publicly funded projects or measures:
+- Helmholtz Earth and Environment DataHub
+  (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+
+Licensed under the HEESIL, Version 1.0 or - as soon they will be
+approved by the "Community" - subsequent versions of the HEESIL
+(the "Licence").
+
+You may not use this work except in compliance with the Licence.
+
+You may obtain a copy of the Licence at:
+https://gitext.gfz-potsdam.de/software/heesil
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the Licence is distributed on an "AS IS" basis,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+implied. See the Licence for the specific language governing
+permissions and limitations under the Licence.
+-->
+<template>
+  <div>
+    <v-card-actions>
+      <v-spacer />
+      <v-btn
+        v-if="isLoggedIn"
+        small
+        text
+        nuxt
+        :to="'/devices/' + deviceId + '/actions'"
+      >
+        cancel
+      </v-btn>
+      <v-btn
+        v-if="isLoggedIn"
+        color="green"
+        small
+        :disabled="isSaving"
+        @click="save"
+      >
+        apply
+      </v-btn>
+    </v-card-actions>
+    <!-- just to be consistent with the new mask, we show the selected action type as an disabled v-select here -->
+    <v-select
+      :value="valueCopy.actionTypeName"
+      :items="[valueCopy.actionTypeName]"
+      :item-text="(x) => x"
+      disabled
+      label="Action Type"
+    />
+    <GenericActionForm
+      ref="genericDeviceActionForm"
+      v-model="valueCopy"
+      :attachments="attachments"
+    />
+    <v-card-actions>
+      <v-spacer />
+      <v-btn
+        v-if="isLoggedIn"
+        small
+        text
+        nuxt
+        :to="'/devices/' + deviceId + '/actions'"
+      >
+        cancel
+      </v-btn>
+      <v-btn
+        v-if="isLoggedIn"
+        color="green"
+        small
+        :disabled="isSaving"
+        @click="save"
+      >
+        apply
+      </v-btn>
+    </v-card-actions>
+  </div>
+</template>
+
+<script lang="ts">
+import { Component, Vue, Prop, Watch } from 'nuxt-property-decorator'
+
+import { Attachment } from '@/models/Attachment'
+import { GenericAction } from '@/models/GenericAction'
+
+import GenericActionForm from '@/components/GenericActionForm.vue'
+
+@Component({
+  components: {
+    GenericActionForm
+  }
+})
+export default class DeviceActionEditPage extends Vue {
+  private valueCopy: GenericAction = new GenericAction()
+  private attachments: Attachment[] = []
+  private _isSaving: boolean = false
+
+  @Prop({
+    default: () => new GenericAction(),
+    required: true,
+    type: Object
+  })
+  readonly value!: GenericAction
+
+  created () {
+    if (this.value) {
+      this.valueCopy = GenericAction.createFromObject(this.value)
+    }
+  }
+
+  async fetch (): Promise<any> {
+    try {
+      this.attachments = await this.$api.devices.findRelatedDeviceAttachments(this.deviceId)
+    } catch (_) {
+      this.$store.commit('snackbar/setError', 'Failed to fetch attachments')
+    }
+  }
+
+  get deviceId (): string {
+    return this.$route.params.deviceId
+  }
+
+  get isLoggedIn (): boolean {
+    return this.$store.getters['oidc/isAuthenticated']
+  }
+
+  get isSaving (): boolean {
+    return this.$data._isSaving
+  }
+
+  set isSaving (value: boolean) {
+    this.$data._isSaving = value
+    this.$emit('showsave', value)
+  }
+
+  save (): void {
+    if (!(this.$refs.genericDeviceActionForm as Vue & { isValid: () => boolean }).isValid()) {
+      this.$store.commit('snackbar/setError', 'Please correct the errors')
+      return
+    }
+    this.isSaving = true
+    this.$api.genericDeviceActions.update(this.deviceId, this.valueCopy).then((action: GenericAction) => {
+      this.$router.push('/devices/' + this.deviceId + '/actions', () => this.$emit('input', action))
+    }).catch(() => {
+      this.$store.commit('snackbar/setError', 'Failed to save the action')
+    }).finally(() => {
+      this.isSaving = false
+    })
+  }
+
+  @Watch('value', { immediate: true, deep: true })
+  // @ts-ignore
+  onValueChanged (val: GenericAction) {
+    this.valueCopy = GenericAction.createFromObject(val)
+  }
+}
+</script>
diff --git a/pages/devices/_deviceId/actions/new.vue b/pages/devices/_deviceId/actions/new.vue
index 65c0f9fcd1338a772360c4e17c60ad5e2859a64f..dc3320194fb0d05b31aaf341c5b66539dfb91b29 100644
--- a/pages/devices/_deviceId/actions/new.vue
+++ b/pages/devices/_deviceId/actions/new.vue
@@ -63,10 +63,11 @@ permissions and limitations under the Licence.
           Add
         </v-btn>
         <v-btn
-          v-else-if="otherChosen"
+          v-else-if="genericActionChosen"
           color="green"
           small
-          @click="addGenericDeviceAction"
+          :disabled="isSaving"
+          @click="addGenericAction"
         >
           Add
         </v-btn>
@@ -74,7 +75,7 @@ permissions and limitations under the Licence.
       <v-card-text>
         <v-select
           v-model="chosenKindOfAction"
-          :items="optionsForActionType"
+          :items="actionTypeItems"
           :item-text="(x) => x.name"
           :item-value="(x) => x"
           clearable
@@ -172,36 +173,17 @@ permissions and limitations under the Licence.
         </v-row>
       </v-card-text>
       <v-card-text
-        v-if="otherChosen"
+        v-if="genericActionChosen"
       >
-        <v-form
-          ref="datesForm"
-          v-model="datesAreValid"
-          @submit.prevent
-        >
-          <v-row>
-            <v-col cols="12" md="6">
-              <DatePicker
-                :value="startDate"
-                label="Start date"
-                :rules="[rules.startDate, rules.startDateNotNull]"
-                @input="setStartDateAndValidate"
-              />
-            </v-col>
-            <v-col cols="12" md="6">
-              <DatePicker
-                :value="endDate"
-                label="End date"
-                :rules="[rules.endDate]"
-                @input="setEndDateAndValidate"
-              />
-            </v-col>
-          </v-row>
-        </v-form>
+        <GenericActionForm
+          ref="genericDeviceActionForm"
+          v-model="genericDeviceAction"
+          :attachments="attachments"
+        />
       </v-card-text>
       <!-- action type independent -->
       <v-card-text
-        v-if="chosenKindOfAction"
+        v-if="chosenKindOfAction && !genericActionChosen"
       >
         <v-row>
           <v-col cols="12" md="12">
@@ -278,10 +260,11 @@ permissions and limitations under the Licence.
           Add
         </v-btn>
         <v-btn
-          v-else-if="otherChosen"
+          v-else-if="genericActionChosen"
           color="green"
           small
-          @click="addGenericDeviceAction"
+          :disabled="isSaving"
+          @click="addGenericAction"
         >
           Add
         </v-btn>
@@ -297,27 +280,29 @@ import { DateTime } from 'luxon'
 import { Contact } from '@/models/Contact'
 import { Attachment } from '@/models/Attachment'
 import { DeviceProperty } from '@/models/DeviceProperty'
+import { GenericAction } from '@/models/GenericAction'
+import { IActionType, ActionType } from '@/models/ActionType'
+
+import { ACTION_TYPE_API_FILTER_DEVICE } from '@/services/cv/ActionTypeApi'
 
 import { dateToString, stringToDate } from '@/utils/dateHelper'
 
+import GenericActionForm from '@/components/GenericActionForm.vue'
 import DatePicker from '@/components/DatePicker.vue'
 
-type KindOfActionType = 'device_calibration' | 'software_update' | 'generic_device_action'
+const KIND_OF_ACTION_TYPE_DEVICE_CALIBRATION = 'device_calibration'
+const KIND_OF_ACTION_TYPE_SOFTWARE_UPDATE = 'software_update'
+const KIND_OF_ACTION_TYPE_GENERIC_DEVICE_ACTION = 'generic_device_action'
+const KIND_OF_ACTION_TYPE_UNKNOWN = 'unknown'
+type KindOfActionType = typeof KIND_OF_ACTION_TYPE_DEVICE_CALIBRATION | typeof KIND_OF_ACTION_TYPE_SOFTWARE_UPDATE | typeof KIND_OF_ACTION_TYPE_GENERIC_DEVICE_ACTION | typeof KIND_OF_ACTION_TYPE_UNKNOWN
 
-interface IGenericActionType {
-  id: string
-  uri: string
-  name: string
-}
-
-interface IOptionsForActionType {
-  id: string
+type IOptionsForActionType = Pick<IActionType, 'id' | 'name' | 'uri'> & {
   kind: KindOfActionType
-  name: string
 }
 
 @Component({
   components: {
+    GenericActionForm,
     DatePicker
   }
 })
@@ -336,13 +321,23 @@ export default class ActionAddPage extends Vue {
   private contactIsValid = true
   private softwareTypeIsValid = true
 
-  private optionsForActionType: IOptionsForActionType[] = [
-    { id: 'device-calibration', kind: 'device_calibration', name: 'Device calibration' },
-    { id: 'software-update', kind: 'software_update', name: 'Software update' },
-    { id: 'generic-action-1', kind: 'generic_device_action', /* uri: 'actionTypes/device_visit', */ name: 'Device visit' },
-    { id: 'generic-action-2', kind: 'generic_device_action', /* uri: 'actionTypes/device_maintainance', */ name: 'Device maintainance' }
+  private specialActionTypes: IOptionsForActionType[] = [
+    {
+      id: 'device_calibration',
+      name: 'Device Calibration',
+      uri: '',
+      kind: KIND_OF_ACTION_TYPE_DEVICE_CALIBRATION
+    },
+    {
+      id: 'software_update',
+      name: 'Software Update',
+      uri: '',
+      kind: KIND_OF_ACTION_TYPE_SOFTWARE_UPDATE
+    }
   ]
 
+  private genericActionTypes: ActionType[] = []
+
   private contacts: Contact[] = []
   private selectedContact: Contact | null = null
   private readonly labelForSelectMeButton = 'Add current user'
@@ -363,6 +358,20 @@ export default class ActionAddPage extends Vue {
   private startDate: DateTime | null = null
   private endDate: DateTime | null = null
 
+  private genericDeviceAction: GenericAction = new GenericAction()
+
+  private _isSaving: boolean = false
+
+  async fetch () {
+    await Promise.all([
+      this.fetchGenericActionTypes()
+    ])
+  }
+
+  async fetchGenericActionTypes (): Promise<any> {
+    this.genericActionTypes = await this.$api.actionTypes.newSearchBuilder().onlyType(ACTION_TYPE_API_FILTER_DEVICE).build().findMatchingAsList()
+  }
+
   mounted () {
     this.$api.contacts.findAll().then((foundContacts) => {
       this.contacts = foundContacts
@@ -403,6 +412,11 @@ export default class ActionAddPage extends Vue {
       if (this.$data._chosenKindOfAction?.kind !== newValue?.kind) {
         this.resetAllActionSpecificInputs()
       }
+      if (this.genericActionChosen) {
+        this.genericDeviceAction = new GenericAction()
+        this.genericDeviceAction.actionTypeName = newValue?.name || ''
+        this.genericDeviceAction.actionTypeUrl = newValue?.uri || ''
+      }
     }
   }
 
@@ -416,15 +430,15 @@ export default class ActionAddPage extends Vue {
   }
 
   get deviceCalibrationChosen () {
-    return this.$data._chosenKindOfAction?.kind === 'device_calibration'
+    return this.$data._chosenKindOfAction?.kind === KIND_OF_ACTION_TYPE_DEVICE_CALIBRATION
   }
 
   get softwareUpdateChosen () {
-    return this.$data._chosenKindOfAction?.kind === 'software_update'
+    return this.$data._chosenKindOfAction?.kind === KIND_OF_ACTION_TYPE_SOFTWARE_UPDATE
   }
 
-  get otherChosen () {
-    return this.$data._chosenKindOfAction?.kind === 'generic_device_action'
+  get genericActionChosen () {
+    return this.$data._chosenKindOfAction?.kind === KIND_OF_ACTION_TYPE_GENERIC_DEVICE_ACTION
   }
 
   getStartDate (): string {
@@ -509,6 +523,15 @@ export default class ActionAddPage extends Vue {
     return this.$route.params.deviceId
   }
 
+  get isSaving (): boolean {
+    return this.$data._isSaving
+  }
+
+  set isSaving (value: boolean) {
+    this.$data._isSaving = value
+    this.$emit('showsave', value)
+  }
+
   addDeviceCalibrationAction () {
     if (!(this.$refs.datesForm as Vue & { validate: () => boolean }).validate()) {
       return
@@ -533,14 +556,40 @@ export default class ActionAddPage extends Vue {
     this.$store.commit('snackbar/setError', 'Not implemented yet')
   }
 
-  addGenericDeviceAction () {
-    if (!(this.$refs.datesForm as Vue & { validate: () => boolean }).validate()) {
+  addGenericAction () {
+    if (!this.isLoggedIn) {
       return
     }
-    if (!(this.$refs.contactForm as Vue & { validate: () => boolean}).validate()) {
+    if (!this.genericDeviceAction) {
       return
     }
-    this.$store.commit('snackbar/setError', 'Not implemented yet')
+    if (!(this.$refs.genericDeviceActionForm as Vue & { isValid: () => boolean }).isValid()) {
+      this.isSaving = false
+      this.$store.commit('snackbar/setError', 'Please correct the errors')
+      return
+    }
+    this.isSaving = true
+    this.$api.genericDeviceActions.add(this.deviceId, this.genericDeviceAction).then((action: GenericAction) => {
+      this.$router.push('/devices/' + this.deviceId + '/actions', () => this.$emit('input', action))
+    }).catch(() => {
+      this.$store.commit('snackbar/setError', 'Failed to save the action')
+    }).finally(() => {
+      this.isSaving = false
+    })
+  }
+
+  get actionTypeItems (): IOptionsForActionType[] {
+    return [
+      ...this.specialActionTypes,
+      ...this.genericActionTypes.map((i) => {
+        return {
+          id: i.id,
+          name: i.name,
+          uri: i.uri,
+          kind: KIND_OF_ACTION_TYPE_GENERIC_DEVICE_ACTION
+        }
+      })
+    ] as IOptionsForActionType[]
   }
 }
 
diff --git a/serializers/jsonapi/ActionTypeSerializer.ts b/serializers/jsonapi/ActionTypeSerializer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..d57e1d11eee7e05501071a835574f15f0ba8d2b4
--- /dev/null
+++ b/serializers/jsonapi/ActionTypeSerializer.ts
@@ -0,0 +1,52 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { ActionType } from '@/models/ActionType'
+
+import {
+  IJsonApiEntityListEnvelope,
+  IJsonApiEntity
+} from '@/serializers/jsonapi/JsonApiTypes'
+
+export class ActionTypeSerializer {
+  convertJsonApiObjectListToModelList (jsonApiObjectList: IJsonApiEntityListEnvelope): ActionType[] {
+    return jsonApiObjectList.data.map(this.convertJsonApiDataToModel.bind(this))
+  }
+
+  convertJsonApiDataToModel (jsonApiData: IJsonApiEntity): ActionType {
+    const id = jsonApiData.id.toString()
+    const name = jsonApiData.attributes.term
+    const url = jsonApiData.links?.self || ''
+    const definition = jsonApiData.attributes.definition
+
+    return ActionType.createWithData(id, name, url, definition)
+  }
+}
diff --git a/serializers/jsonapi/ContactSerializer.ts b/serializers/jsonapi/ContactSerializer.ts
index afe5340ca72f804d4afb7223395fef0bc42e1a64..b697a3df62c09d3cafb67b28e2bcedddb9237605 100644
--- a/serializers/jsonapi/ContactSerializer.ts
+++ b/serializers/jsonapi/ContactSerializer.ts
@@ -185,7 +185,7 @@ export class ContactSerializer {
     }
   }
 
-  convertJsonApiRelationshipsSingleModel (relationships: IJsonApiTypedEntityWithoutDetailsDataDict, included: IJsonApiEntityWithOptionalAttributes[]): IContactAndMissing {
+  convertJsonApiRelationshipsSingleModel (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): IContactAndMissing {
     let relationContactId: string = ''
     if (relationships.contact) {
       const contactObject = relationships.contact as IJsonApiEntityWithoutDetailsDataDict
diff --git a/serializers/jsonapi/GenericActionAttachmentSerializer.ts b/serializers/jsonapi/GenericActionAttachmentSerializer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2500a9899523dbc447cf23c6a7bf7591aca39cd6
--- /dev/null
+++ b/serializers/jsonapi/GenericActionAttachmentSerializer.ts
@@ -0,0 +1,165 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { Attachment } from '@/models/Attachment'
+
+import {
+  IJsonApiEntityWithOptionalId,
+  IJsonApiEntityWithOptionalAttributes,
+  IJsonApiRelationships,
+  IJsonApiEntityWithoutDetails,
+  IJsonApiEntityWithoutDetailsDataDictList
+} from '@/serializers/jsonapi/JsonApiTypes'
+
+import { DeviceAttachmentSerializer } from '@/serializers/jsonapi/DeviceAttachmentSerializer'
+
+export interface IGenericActionAttachmentSerializer {
+  targetType: string
+  convertModelToJsonApiData (attachment: Attachment, actionId: string): IJsonApiEntityWithOptionalId
+  convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): Attachment[]
+  getActionTypeName (): string
+  getActionAttachmentTypeName (): string
+  getActionAttachmentTypeNamePlural (): string
+  getAttachmentTypeName (): string
+}
+
+export abstract class AbstractGenericActionAttachmentSerializer implements IGenericActionAttachmentSerializer {
+  private attachmentSerializer: DeviceAttachmentSerializer = new DeviceAttachmentSerializer()
+
+  abstract get targetType (): string
+
+  convertModelToJsonApiData (attachment: Attachment, actionId: string): IJsonApiEntityWithOptionalId {
+    /**
+     * 2021-05-07 mha:
+     * We build the relation to the action by hand instead of using the
+     * GenericActionSerializer, to avoid circular references.  We also
+     * build the relation to the attachment by hand instead of using the
+     * DeviceAttachmentSerializer, which uses 'device_attachment' as its
+     * property whereas we need 'attachment' as the property and
+     * 'device_attachment' as the type.
+     */
+    const entityType = this.getActionAttachmentTypeName()
+    const actionType = this.getActionTypeName()
+    const attachmentType = this.getAttachmentTypeName()
+    const data: IJsonApiEntityWithOptionalId = {
+      type: entityType,
+      attributes: {},
+      relationships: {
+        action: {
+          data: {
+            type: actionType,
+            id: actionId
+          }
+        },
+        attachment: {
+          data: {
+            type: attachmentType,
+            id: attachment.id || ''
+          }
+        }
+      }
+    }
+    return data
+  }
+
+  convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): Attachment[] {
+    const actionAttachmentIds = []
+    const entityType = this.getActionAttachmentTypeName()
+    const typePlural = this.getActionAttachmentTypeNamePlural()
+    if (relationships[typePlural]) {
+      const attachmentObject = relationships[typePlural] as IJsonApiEntityWithoutDetailsDataDictList
+      if (attachmentObject.data && (attachmentObject.data as IJsonApiEntityWithoutDetails[]).length > 0) {
+        for (const relationShipAttachmentData of attachmentObject.data as IJsonApiEntityWithoutDetails[]) {
+          const actionAttachmentId = relationShipAttachmentData.id
+          actionAttachmentIds.push(actionAttachmentId)
+        }
+      }
+    }
+
+    const attachmentIds = []
+    if (included && included.length > 0) {
+      for (const includedEntry of included) {
+        if (includedEntry.type === entityType) {
+          const actionAttachmentId = includedEntry.id
+          if (actionAttachmentIds.includes(actionAttachmentId)) {
+            if ((includedEntry.relationships?.attachment?.data as IJsonApiEntityWithoutDetails | undefined)?.id) {
+              attachmentIds.push((includedEntry.relationships?.attachment?.data as IJsonApiEntityWithoutDetails).id)
+            }
+          }
+        }
+      }
+    }
+
+    const attachmentType = this.getAttachmentTypeName()
+    const attachments: Attachment[] = []
+    if (included && included.length > 0) {
+      for (const includedEntry of included) {
+        if (includedEntry.type === attachmentType) {
+          const attachmentId = includedEntry.id
+          if (attachmentIds.includes(attachmentId)) {
+            const attachment = this.attachmentSerializer.convertJsonApiDataToModel(includedEntry)
+            attachments.push(attachment)
+          }
+        }
+      }
+    }
+
+    return attachments
+  }
+
+  getActionTypeName (): string {
+    return 'generic_' + this.targetType + '_action'
+  }
+
+  getActionAttachmentTypeName (): string {
+    return 'generic_' + this.targetType + '_action_attachment'
+  }
+
+  getActionAttachmentTypeNamePlural (): string {
+    return this.getActionAttachmentTypeName() + 's'
+  }
+
+  getAttachmentTypeName (): string {
+    return this.targetType + '_attachment'
+  }
+}
+
+export class GenericDeviceActionAttachmentSerializer extends AbstractGenericActionAttachmentSerializer {
+  get targetType (): string {
+    return 'device'
+  }
+}
+
+export class GenericPlatformActionAttachmentSerializer extends AbstractGenericActionAttachmentSerializer {
+  get targetType (): string {
+    return 'platform'
+  }
+}
diff --git a/serializers/jsonapi/GenericActionSerializer.ts b/serializers/jsonapi/GenericActionSerializer.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2360bb662a76eb40c8884a06b2b6bbdd63b2423c
--- /dev/null
+++ b/serializers/jsonapi/GenericActionSerializer.ts
@@ -0,0 +1,295 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { DateTime } from 'luxon'
+import { GenericAction, IGenericAction } from '@/models/GenericAction'
+import { Attachment } from '@/models/Attachment'
+import {
+  IJsonApiEntityEnvelope,
+  IJsonApiEntityListEnvelope,
+  IJsonApiEntityWithOptionalId,
+  IJsonApiEntityWithOptionalAttributes,
+  IJsonApiEntityWithoutDetails,
+  IJsonApiEntityWithoutDetailsDataDictList,
+  IJsonApiRelationships
+} from '@/serializers/jsonapi/JsonApiTypes'
+
+import {
+  ContactSerializer,
+  IContactAndMissing
+} from '@/serializers/jsonapi/ContactSerializer'
+
+import {
+  IGenericActionAttachmentSerializer,
+  GenericDeviceActionAttachmentSerializer,
+  GenericPlatformActionAttachmentSerializer
+} from '@/serializers/jsonapi/GenericActionAttachmentSerializer'
+
+export interface IMissingGenericActionData {
+  ids: string[]
+}
+
+export interface IGenericActionsAndMissing {
+  genericDeviceActions: GenericAction[]
+  missing: IMissingGenericActionData
+}
+
+export interface IGenericActionAttachmentRelation {
+  genericActionAttachmentId: string
+  attachmentId: string
+}
+
+export interface IGenericActionSerializer {
+  targetType: string
+  attachmentSerializer: IGenericActionAttachmentSerializer
+  convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): GenericAction
+  convertJsonApiDataToModel (jsonApiData: IJsonApiEntityWithOptionalAttributes, included: IJsonApiEntityWithOptionalAttributes[]): GenericAction
+  convertJsonApiObjectListToModelList (jsonApiObjectList: IJsonApiEntityListEnvelope): GenericAction[]
+  convertModelToJsonApiData (action: GenericAction, deviceOrPlatformId: string): IJsonApiEntityWithOptionalId
+  convertModelToJsonApiRelationshipObject (action: IGenericAction): IJsonApiRelationships
+  convertModelToTupleWithIdAndType (action: IGenericAction): IJsonApiEntityWithoutDetails
+  convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): IGenericActionsAndMissing
+  convertJsonApiIncludedGenericActionAttachmentsToIdList (included: IJsonApiEntityWithOptionalAttributes[]): IGenericActionAttachmentRelation[]
+  getActionTypeName (): string
+  getActionTypeNamePlural (): string
+  getActionAttachmentTypeName (): string
+}
+
+export abstract class AbstractGenericActionSerializer implements IGenericActionSerializer {
+  private contactSerializer: ContactSerializer = new ContactSerializer()
+
+  abstract get targetType (): string
+  abstract get attachmentSerializer (): IGenericActionAttachmentSerializer
+
+  convertJsonApiObjectToModel (jsonApiObject: IJsonApiEntityEnvelope): GenericAction {
+    const data = jsonApiObject.data
+    const included = jsonApiObject.included || []
+    return this.convertJsonApiDataToModel(data, included)
+  }
+
+  convertJsonApiDataToModel (jsonApiData: IJsonApiEntityWithOptionalAttributes, included: IJsonApiEntityWithOptionalAttributes[]): GenericAction {
+    const attributes = jsonApiData.attributes
+    const newEntry = GenericAction.createEmpty()
+
+    newEntry.id = jsonApiData.id.toString()
+    if (attributes) {
+      newEntry.description = attributes.description || ''
+      newEntry.actionTypeName = attributes.action_type_name || ''
+      newEntry.actionTypeUrl = attributes.action_type_uri || ''
+      newEntry.beginDate = attributes.begin_date ? DateTime.fromISO(attributes.begin_date, { zone: 'UTC' }) : null
+      newEntry.endDate = attributes.end_date ? DateTime.fromISO(attributes.end_date, { zone: 'UTC' }) : null
+    }
+
+    const relationships = jsonApiData.relationships || {}
+
+    const contactWithMissing: IContactAndMissing = this.contactSerializer.convertJsonApiRelationshipsSingleModel(relationships, included)
+    if (contactWithMissing.contact) {
+      newEntry.contact = contactWithMissing.contact
+    }
+
+    const attachments: Attachment[] = this.attachmentSerializer.convertJsonApiRelationshipsModelList(relationships, included)
+    if (attachments.length) {
+      newEntry.attachments = attachments
+    }
+
+    return newEntry
+  }
+
+  convertJsonApiObjectListToModelList (jsonApiObjectList: IJsonApiEntityListEnvelope): GenericAction[] {
+    const included = jsonApiObjectList.included || []
+    return jsonApiObjectList.data.map((model) => {
+      return this.convertJsonApiDataToModel(model, included)
+    })
+  }
+
+  convertModelToJsonApiData (action: GenericAction, deviceOrPlatformId: string): IJsonApiEntityWithOptionalId {
+    const data: IJsonApiEntityWithOptionalId = {
+      type: this.getActionTypeName(),
+      attributes: {
+        description: action.description,
+        action_type_name: action.actionTypeName,
+        action_type_uri: action.actionTypeUrl,
+        begin_date: action.beginDate != null ? action.beginDate.setZone('UTC').toISO() : null,
+        end_date: action.endDate != null ? action.endDate.setZone('UTC').toISO() : null
+      },
+      relationships: {
+        [this.targetType]: {
+          data: {
+            type: this.targetType,
+            id: deviceOrPlatformId
+          }
+        }
+      }
+    }
+    if (action.id) {
+      data.id = action.id
+    }
+    if (action.contact && action.contact.id) {
+      const contactRelationship = this.contactSerializer.convertModelToJsonApiRelationshipObject(action.contact)
+      data.relationships = {
+        ...data.relationships,
+        ...contactRelationship
+      }
+    }
+    // Note: Attachments are not included and must be send to the backend with
+    // a relation to the action after this action was saved
+    return data
+  }
+
+  convertModelToJsonApiRelationshipObject (action: IGenericAction): IJsonApiRelationships {
+    return {
+      [this.getActionTypeName()]: {
+        data: this.convertModelToTupleWithIdAndType(action)
+      }
+    }
+  }
+
+  convertModelToTupleWithIdAndType (action: IGenericAction): IJsonApiEntityWithoutDetails {
+    return {
+      id: action.id || '',
+      type: this.getActionTypeName()
+    }
+  }
+
+  convertJsonApiRelationshipsModelList (relationships: IJsonApiRelationships, included: IJsonApiEntityWithOptionalAttributes[]): IGenericActionsAndMissing {
+    const actionIds = []
+    const type = this.getActionTypeName()
+    const typePlural = this.getActionTypeNamePlural()
+    if (relationships[typePlural]) {
+      const actionObject = relationships[typePlural] as IJsonApiEntityWithoutDetailsDataDictList
+      if (actionObject.data && (actionObject.data as IJsonApiEntityWithoutDetails[]).length > 0) {
+        for (const relationShipActionData of actionObject.data) {
+          const actionId = relationShipActionData.id
+          actionIds.push(actionId)
+        }
+      }
+    }
+
+    const possibleActions: { [key: string]: GenericAction } = {}
+    if (included && included.length > 0) {
+      for (const includedEntry of included) {
+        if (includedEntry.type === type) {
+          const actionId = includedEntry.id
+          if (actionIds.includes(actionId)) {
+            const action = this.convertJsonApiDataToModel(includedEntry, [])
+            possibleActions[actionId] = action
+          }
+        }
+      }
+    }
+
+    const actions = []
+    const missingDataForActionIds = []
+
+    for (const actionId of actionIds) {
+      if (possibleActions[actionId]) {
+        actions.push(possibleActions[actionId])
+      } else {
+        missingDataForActionIds.push(actionId)
+      }
+    }
+
+    return {
+      genericDeviceActions: actions,
+      missing: {
+        ids: missingDataForActionIds
+      }
+    }
+  }
+
+  convertJsonApiIncludedGenericActionAttachmentsToIdList (included: IJsonApiEntityWithOptionalAttributes[]): IGenericActionAttachmentRelation[] {
+    const linkedAttachments: IGenericActionAttachmentRelation[] = []
+    const type = this.getActionAttachmentTypeName()
+    included.forEach((i) => {
+      if (!i.id) {
+        return
+      }
+      if (i.type !== type) {
+        return
+      }
+      if (!i.relationships?.attachment || !i.relationships?.attachment.data || !(i.relationships?.attachment.data as IJsonApiEntityWithoutDetails).id) {
+        return
+      }
+      const attachmentId: string = (i.relationships.attachment.data as IJsonApiEntityWithoutDetails).id
+      linkedAttachments.push({
+        genericActionAttachmentId: i.id,
+        attachmentId
+      })
+    })
+    return linkedAttachments
+  }
+
+  getActionTypeName (): string {
+    return 'generic_' + this.targetType + '_action'
+  }
+
+  getActionTypeNamePlural (): string {
+    return this.getActionTypeName() + 's'
+  }
+
+  getActionAttachmentTypeName (): string {
+    return 'generic_' + this.targetType + '_action_attachment'
+  }
+}
+
+export class GenericDeviceActionSerializer extends AbstractGenericActionSerializer {
+  private _attachmentSerializer: IGenericActionAttachmentSerializer
+
+  get targetType (): string {
+    return 'device'
+  }
+
+  get attachmentSerializer (): IGenericActionAttachmentSerializer {
+    return this._attachmentSerializer
+  }
+
+  constructor () {
+    super()
+    this._attachmentSerializer = new GenericDeviceActionAttachmentSerializer()
+  }
+}
+
+export class GenericPlatformActionSerializer extends AbstractGenericActionSerializer {
+  private _attachmentSerializer: IGenericActionAttachmentSerializer
+
+  get targetType (): string {
+    return 'platform'
+  }
+
+  get attachmentSerializer (): IGenericActionAttachmentSerializer {
+    return this._attachmentSerializer
+  }
+
+  constructor () {
+    super()
+    this._attachmentSerializer = new GenericPlatformActionAttachmentSerializer()
+  }
+}
diff --git a/services/Api.ts b/services/Api.ts
index 2ba69c48a03819d1c7ca08f1a40b617f498f6860..551e38f1a6e40d1dd7b065e61f0e6ce4af9ddacd 100644
--- a/services/Api.ts
+++ b/services/Api.ts
@@ -40,6 +40,8 @@ import { ConfigurationStatusApi } from '@/services/sms/ConfigurationStatusApi'
 import { CustomfieldsApi } from '@/services/sms/CustomfieldsApi'
 import { DeviceAttachmentApi } from '@/services/sms/DeviceAttachmentApi'
 import { PlatformAttachmentApi } from '@/services/sms/PlatformAttachmentApi'
+import { GenericDeviceActionApi } from '@/services/sms/GenericDeviceActionApi'
+import { GenericDeviceActionAttachmentApi } from '@/services/sms/GenericDeviceActionAttachmentApi'
 
 import { CompartmentApi } from '@/services/cv/CompartmentApi'
 import { DeviceTypeApi } from '@/services/cv/DeviceTypeApi'
@@ -50,6 +52,7 @@ import { SamplingMediaApi } from '@/services/cv/SamplingMediaApi'
 import { StatusApi } from '@/services/cv/StatusApi'
 import { UnitApi } from '@/services/cv/UnitApi'
 import { MeasuredQuantityUnitApi } from '@/services/cv/MeasuredQuantityUnitApi'
+import { ActionTypeApi } from '@/services/cv/ActionTypeApi'
 
 import { ProjectApi } from '@/services/project/ProjectApi'
 
@@ -66,6 +69,8 @@ export class Api {
   private readonly _deviceAttachmentApi: DeviceAttachmentApi
   private readonly _platformAttachmentApi: PlatformAttachmentApi
   private readonly _devicePropertyApi: DevicePropertyApi
+  private readonly _genericDeviceActionApi: GenericDeviceActionApi
+  private readonly _genericDeviceActionAttachmentApi: GenericDeviceActionAttachmentApi
 
   private readonly _manufacturerApi: ManufacturerApi
   private readonly _platformTypeApi: PlatformTypeApi
@@ -76,6 +81,7 @@ export class Api {
   private readonly _propertyApi: PropertyApi
   private readonly _unitApi: UnitApi
   private readonly _measuredQuantityUnitApi: MeasuredQuantityUnitApi
+  private readonly _actionTypeApi: ActionTypeApi
 
   private readonly _projectApi: ProjectApi
 
@@ -123,6 +129,15 @@ export class Api {
       this.createAxios(smsBaseUrl, '/device-properties', smsConfig, getIdToken)
     )
 
+    this._genericDeviceActionAttachmentApi = new GenericDeviceActionAttachmentApi(
+      this.createAxios(smsBaseUrl, '/generic-device-action-attachments', smsConfig, getIdToken)
+    )
+
+    this._genericDeviceActionApi = new GenericDeviceActionApi(
+      this.createAxios(smsBaseUrl, '/generic-device-actions', smsConfig, getIdToken),
+      this._genericDeviceActionAttachmentApi
+    )
+
     // and here we can set settings for all the cv api calls
     const cvConfig: AxiosRequestConfig = {
       headers: {
@@ -159,6 +174,9 @@ export class Api {
     this._measuredQuantityUnitApi = new MeasuredQuantityUnitApi(
       this.createAxios(cvBaseUrl, '/measuredquantityunits/', cvConfig)
     )
+    this._actionTypeApi = new ActionTypeApi(
+      this.createAxios(cvBaseUrl, '/actiontypes/', cvConfig)
+    )
 
     this._projectApi = new ProjectApi()
   }
@@ -219,6 +237,14 @@ export class Api {
     return this._devicePropertyApi
   }
 
+  get genericDeviceActions (): GenericDeviceActionApi {
+    return this._genericDeviceActionApi
+  }
+
+  get genericDeviceActionAttachments (): GenericDeviceActionAttachmentApi {
+    return this._genericDeviceActionAttachmentApi
+  }
+
   get contacts (): ContactApi {
     return this._contactApi
   }
@@ -259,6 +285,10 @@ export class Api {
     return this._measuredQuantityUnitApi
   }
 
+  get actionTypes (): ActionTypeApi {
+    return this._actionTypeApi
+  }
+
   get projects (): ProjectApi {
     return this._projectApi
   }
diff --git a/services/cv/ActionTypeApi.ts b/services/cv/ActionTypeApi.ts
new file mode 100644
index 0000000000000000000000000000000000000000..c0b1f8962ddef851d0e497ab8e622c01ddda2417
--- /dev/null
+++ b/services/cv/ActionTypeApi.ts
@@ -0,0 +1,155 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { AxiosInstance } from 'axios'
+
+import { ActionType } from '@/models/ActionType'
+import { ActionTypeSerializer } from '@/serializers/jsonapi/ActionTypeSerializer'
+import { CVApi } from '@/services/cv/CVApi'
+
+import { IPaginationLoader } from '@/utils/PaginatedLoader'
+
+export class ActionTypeApi extends CVApi<ActionType> {
+  private serializer: ActionTypeSerializer
+
+  constructor (axiosInstance: AxiosInstance) {
+    super(axiosInstance)
+    this.serializer = new ActionTypeSerializer()
+  }
+
+  newSearchBuilder (): ActionTypeSearchBuilder {
+    return new ActionTypeSearchBuilder(this.axiosApi, this.serializer)
+  }
+
+  findAll (): Promise<ActionType[]> {
+    return this.newSearchBuilder().build().findMatchingAsList()
+  }
+
+  findAllPaginated (pageSize: number = 100): Promise<ActionType[]> {
+    return this.newSearchBuilder().build().findMatchingAsPaginationLoader(pageSize).then(loader => this.loadPaginated(loader))
+  }
+}
+
+export const ACTION_TYPE_API_FILTER_DEVICE = 'Device'
+export const ACTION_TYPE_API_FILTER_PLATFORM = 'Platform'
+export const ACTION_TYPE_API_FILTER_CONFIGURATION = 'Configuration'
+export type ActionTypeApiFilterType = typeof ACTION_TYPE_API_FILTER_DEVICE | typeof ACTION_TYPE_API_FILTER_PLATFORM | typeof ACTION_TYPE_API_FILTER_CONFIGURATION
+
+export class ActionTypeSearchBuilder {
+  private axiosApi: AxiosInstance
+  private serializer: ActionTypeSerializer
+  private actionTypeFilter: ActionTypeApiFilterType | undefined
+
+  constructor (axiosApi: AxiosInstance, serializer: ActionTypeSerializer) {
+    this.axiosApi = axiosApi
+    this.serializer = serializer
+  }
+
+  onlyType (actionType: ActionTypeApiFilterType): ActionTypeSearchBuilder {
+    this.actionTypeFilter = actionType
+    return this
+  }
+
+  build (): ActionTypeSearcher {
+    return new ActionTypeSearcher(this.axiosApi, this.serializer, this.actionTypeFilter)
+  }
+}
+
+export class ActionTypeSearcher {
+  private axiosApi: AxiosInstance
+  private serializer: ActionTypeSerializer
+  private actionTypeFilter: ActionTypeApiFilterType | undefined
+
+  constructor (axiosApi: AxiosInstance, serializer: ActionTypeSerializer, actionType?: ActionTypeApiFilterType) {
+    this.axiosApi = axiosApi
+    this.serializer = serializer
+    if (actionType) {
+      this.actionTypeFilter = actionType
+    }
+  }
+
+  private findAllOnPage (page: number, pageSize: number): Promise<IPaginationLoader<ActionType>> {
+    const params: { [idx: string]: any } = {
+      'page[size]': pageSize,
+      'page[number]': page,
+      'filter[status.iexact]': 'ACCEPTED',
+      sort: 'term'
+    }
+    if (this.actionTypeFilter) {
+      params['filter[action_category__term]'] = this.actionTypeFilter
+    }
+    return this.axiosApi.get(
+      '',
+      {
+        params
+      }
+    ).then((rawResponse) => {
+      const response = rawResponse.data
+      const elements: ActionType[] = this.serializer.convertJsonApiObjectListToModelList(response)
+      const totalCount = response.meta.count
+
+      let funToLoadNext = null
+      if (response.meta.pagination.page < response.meta.pagination.pages) {
+        funToLoadNext = () => this.findAllOnPage(page + 1, pageSize)
+      }
+
+      return {
+        elements,
+        totalCount,
+        funToLoadNext
+      }
+    })
+  }
+
+  findMatchingAsList (): Promise<ActionType[]> {
+    const params: { [idx: string]: any } = {
+      'page[size]': 10000,
+      'filter[status.iexact]': 'ACCEPTED',
+      sort: 'term'
+    }
+    if (this.actionTypeFilter) {
+      params['filter[action_category__term]'] = this.actionTypeFilter
+    }
+    return this.axiosApi.get(
+      '',
+      {
+        params
+      }
+    ).then((rawResponse) => {
+      const response = rawResponse.data
+      return this.serializer.convertJsonApiObjectListToModelList(response)
+    })
+  }
+
+  findMatchingAsPaginationLoader (pageSize: number): Promise<IPaginationLoader<ActionType>> {
+    return this.findAllOnPage(1, pageSize)
+  }
+}
diff --git a/services/sms/DeviceApi.ts b/services/sms/DeviceApi.ts
index 914c16848cb76efc4597bd09c414364607894bd6..f8f5e6deca33fff04e58022cd47fae6b6cae13ab 100644
--- a/services/sms/DeviceApi.ts
+++ b/services/sms/DeviceApi.ts
@@ -39,11 +39,13 @@ import { DeviceProperty } from '@/models/DeviceProperty'
 import { DeviceType } from '@/models/DeviceType'
 import { Manufacturer } from '@/models/Manufacturer'
 import { Status } from '@/models/Status'
+import { GenericAction } from '@/models/GenericAction'
 
 import { ContactSerializer } from '@/serializers/jsonapi/ContactSerializer'
 import { CustomTextFieldSerializer } from '@/serializers/jsonapi/CustomTextFieldSerializer'
 import { DeviceAttachmentSerializer } from '@/serializers/jsonapi/DeviceAttachmentSerializer'
 import { DevicePropertySerializer } from '@/serializers/jsonapi/DevicePropertySerializer'
+import { GenericDeviceActionSerializer } from '@/serializers/jsonapi/GenericActionSerializer'
 
 import { IFlaskJSONAPIFilter } from '@/utils/JSONApiInterfaces'
 
@@ -190,6 +192,20 @@ export class DeviceApi {
       return new DevicePropertySerializer().convertJsonApiObjectListToModelList(rawServerResponse.data)
     })
   }
+
+  findRelatedGenericActions (deviceId: string): Promise<GenericAction[]> {
+    const url = deviceId + '/generic-device-actions'
+    const params = {
+      'page[size]': 10000,
+      include: [
+        'contact',
+        'generic_device_action_attachments.attachment'
+      ].join(',')
+    }
+    return this.axiosApi.get(url, { params }).then((rawServerResponse) => {
+      return new GenericDeviceActionSerializer().convertJsonApiObjectListToModelList(rawServerResponse.data)
+    })
+  }
 }
 
 export class DeviceSearchBuilder {
diff --git a/services/sms/GenericDeviceActionApi.ts b/services/sms/GenericDeviceActionApi.ts
new file mode 100644
index 0000000000000000000000000000000000000000..4faf5eb067408d8b836f2b9d53b93b26de93619e
--- /dev/null
+++ b/services/sms/GenericDeviceActionApi.ts
@@ -0,0 +1,146 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { AxiosInstance } from 'axios'
+
+import { Attachment } from '@/models/Attachment'
+import { GenericAction } from '@/models/GenericAction'
+import { GenericDeviceActionAttachmentApi } from '@/services/sms/GenericDeviceActionAttachmentApi'
+import { IGenericActionSerializer, GenericDeviceActionSerializer } from '@/serializers/jsonapi/GenericActionSerializer'
+
+export class GenericDeviceActionApi {
+  private axiosApi: AxiosInstance
+  private serializer: IGenericActionSerializer
+  private attachmentApi: GenericDeviceActionAttachmentApi
+
+  constructor (axiosInstance: AxiosInstance, attachmentApi: GenericDeviceActionAttachmentApi) {
+    this.axiosApi = axiosInstance
+    this.serializer = new GenericDeviceActionSerializer()
+    this.attachmentApi = attachmentApi
+  }
+
+  async findById (id: string): Promise<GenericAction> {
+    const response = await this.axiosApi.get(id, {
+      params: {
+        include: [
+          'contact',
+          'generic_device_action_attachments.attachment'
+        ].join(',')
+      }
+    })
+    const data = response.data
+    return this.serializer.convertJsonApiObjectToModel(data)
+  }
+
+  deleteById (id: string): Promise<void> {
+    return this.axiosApi.delete<string, void>(id)
+  }
+
+  async add (deviceId: string, action: GenericAction): Promise<GenericAction> {
+    const url = ''
+    const data = this.serializer.convertModelToJsonApiData(action, deviceId)
+    const response = await this.axiosApi.post(url, { data })
+    const savedAction = this.serializer.convertJsonApiObjectToModel(response.data)
+    // save every attachment as an GenericActionAttachment
+    if (savedAction.id) {
+      const promises = action.attachments.map((attachment: Attachment) => this.attachmentApi.add(savedAction.id as string, attachment))
+      await Promise.all(promises)
+    }
+    return savedAction
+  }
+
+  async update (deviceId: string, action: GenericAction): Promise<GenericAction> {
+    if (!action.id) {
+      throw new Error('no id for the GenericAction')
+    }
+    // load the stored action to get a list of the generic device action attachments before the update
+    const attRawResponse = await this.axiosApi.get(action.id, {
+      params: {
+        include: [
+          'generic_device_action_attachments.attachment'
+        ].join(',')
+      }
+    })
+    const attResponseData = attRawResponse.data
+    const included = attResponseData.included
+
+    // get the relations between attachments and generic device action attachments
+    const linkedAttachments: { [attachmentId: string]: string } = {}
+    if (included) {
+      const relations = this.serializer.convertJsonApiIncludedGenericActionAttachmentsToIdList(included)
+      // convert to object to gain faster access to its members
+      relations.forEach((rel) => {
+        linkedAttachments[rel.attachmentId] = rel.genericActionAttachmentId
+      })
+    }
+
+    // update the action
+    const data = this.serializer.convertModelToJsonApiData(action, deviceId)
+    const actionResponse = await this.axiosApi.patch(action.id, { data })
+
+    // find new attachments
+    const newAttachments: Attachment[] = []
+    action.attachments.forEach((attachment: Attachment) => {
+      if (attachment.id && linkedAttachments[attachment.id]) {
+        return
+      }
+      newAttachments.push(attachment)
+    })
+
+    // find deleted attachments
+    const genericDeviceActionAttachmentsToDelete: string[] = []
+    for (const attachmentId in linkedAttachments) {
+      if (action.attachments.find((i: Attachment) => i.id === attachmentId)) {
+        continue
+      }
+      genericDeviceActionAttachmentsToDelete.push(linkedAttachments[attachmentId])
+    }
+
+    // when there are no new attachments, newPromises is empty, which is okay
+    const newPromises = newAttachments.map((attachment: Attachment) => this.attachmentApi.add(action.id as string, attachment))
+    // when there are no deleted attachments, deletedPromises is empty, which is okay
+    const deletedPromises = genericDeviceActionAttachmentsToDelete.map((id: string) => this.attachmentApi.delete(id))
+    await Promise.all([...deletedPromises, ...newPromises])
+
+    return this.serializer.convertJsonApiObjectToModel(actionResponse.data)
+  }
+
+  findRelatedGenericActionAttachments (actionId: string): Promise<GenericAction[]> {
+    const url = actionId + '/generic-device-action-attachments'
+    const params = {
+      'page[size]': 10000,
+      include: 'attachment'
+    }
+    return this.axiosApi.get(url, { params }).then((rawServerResponse) => {
+      return this.serializer.convertJsonApiObjectListToModelList(rawServerResponse.data)
+    })
+  }
+}
diff --git a/services/sms/GenericDeviceActionAttachmentApi.ts b/services/sms/GenericDeviceActionAttachmentApi.ts
new file mode 100644
index 0000000000000000000000000000000000000000..b812717b3f39c75822eedf5d13a083103ec917d8
--- /dev/null
+++ b/services/sms/GenericDeviceActionAttachmentApi.ts
@@ -0,0 +1,55 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { AxiosInstance } from 'axios'
+
+import { Attachment } from '@/models/Attachment'
+import { IGenericActionAttachmentSerializer, GenericDeviceActionAttachmentSerializer } from '@/serializers/jsonapi/GenericActionAttachmentSerializer'
+
+export class GenericDeviceActionAttachmentApi {
+  private axiosApi: AxiosInstance
+  private serializer: IGenericActionAttachmentSerializer
+
+  constructor (axiosInstance: AxiosInstance) {
+    this.axiosApi = axiosInstance
+    this.serializer = new GenericDeviceActionAttachmentSerializer()
+  }
+
+  async add (actionId: string, attachment: Attachment): Promise<any> {
+    const url = ''
+    const data = this.serializer.convertModelToJsonApiData(attachment, actionId)
+    await this.axiosApi.post(url, { data })
+  }
+
+  async delete (id: string): Promise<void> {
+    return await this.axiosApi.delete<string, void>(id)
+  }
+}
diff --git a/test/modelUtils/Compareables.test.ts b/test/modelUtils/Compareables.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..13a3560bb25bde1103a03195ac5540d740e6de53
--- /dev/null
+++ b/test/modelUtils/Compareables.test.ts
@@ -0,0 +1,97 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { DateTime } from 'luxon'
+import { IDateCompareable, isDateCompareable, DateComparator } from '@/modelUtils/Compareables'
+
+describe('DateCompareables and DateComparator', () => {
+  describe('DateCompareable', () => {
+    describe('#isDateCompareable', () => {
+      it('should return true when an object implements a `date` property of type DateTime', () => {
+        const dateObject: IDateCompareable = {
+          date: DateTime.now()
+        }
+        expect(isDateCompareable(dateObject)).toBeTruthy()
+      })
+      it('should return false when an object does not implement a `date` property', () => {
+        const falseObject = {
+          foo: 'bar'
+        }
+        // @ts-ignore
+        expect(isDateCompareable(falseObject)).toBeFalsy()
+      })
+      it('should return false when an object implements a `date` property which is not of type DateTime', () => {
+        const simpleObject = {
+          date: 'foo'
+        }
+        // @ts-ignore
+        expect(isDateCompareable(simpleObject)).toBeFalsy()
+      })
+    })
+  })
+  describe('DateComparator', () => {
+    describe('#compare', () => {
+      it('should return -1 when first argument is less than second argument', () => {
+        const a = {
+          date: DateTime.fromISO('2020-06-01T10:00:00.000')
+        }
+        const b = {
+          date: DateTime.fromISO('2020-06-02T10:00:00.000')
+        }
+        const comparator = new DateComparator()
+
+        expect(comparator.compare(a, b)).toBe(-1)
+      })
+      it('should return 1 when first argument is greater than second argument', () => {
+        const a = {
+          date: DateTime.fromISO('2020-06-02T10:00:00.000')
+        }
+        const b = {
+          date: DateTime.fromISO('2020-06-01T10:00:00.000')
+        }
+        const comparator = new DateComparator()
+
+        expect(comparator.compare(a, b)).toBe(1)
+      })
+      it('should return 0 when both arguments are equal', () => {
+        const a = {
+          date: DateTime.fromISO('2020-06-02T10:00:01.000')
+        }
+        const b = {
+          date: DateTime.fromISO('2020-06-02T10:00:01.000')
+        }
+        const comparator = new DateComparator()
+
+        expect(comparator.compare(a, b)).toBe(0)
+      })
+    })
+  })
+})
diff --git a/test/models/GenericAction.test.ts b/test/models/GenericAction.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..e54200bf21b1f07899c59b1914d5709a53a14b82
--- /dev/null
+++ b/test/models/GenericAction.test.ts
@@ -0,0 +1,74 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { DateTime } from 'luxon'
+import { Attachment } from '@/models/Attachment'
+import { Contact } from '@/models/Contact'
+import { GenericAction } from '@/models/GenericAction'
+
+describe('GenericAction', () => {
+  test('create a GenericAction from an object', () => {
+    const date1 = DateTime.fromISO('2021-05-27')
+    const date2 = DateTime.fromISO('2021-05-28')
+
+    const attachment = new Attachment()
+    attachment.id = '1'
+    attachment.label = 'an attachment'
+    attachment.url = 'https://foo/baz'
+
+    const contact = new Contact()
+    contact.givenName = 'Homer'
+    contact.familyName = 'Simpson'
+    contact.email = 'homer.simpson@springfield.com'
+
+    const action = GenericAction.createFromObject({
+      id: '1',
+      description: 'This is a generic action description',
+      actionTypeName: 'Generic Device Action',
+      actionTypeUrl: 'https://foo/bar',
+      beginDate: date1,
+      endDate: date2,
+      contact,
+      attachments: [attachment]
+    })
+
+    expect(typeof action).toBe('object')
+    expect(action).toHaveProperty('id', '1')
+    expect(action).toHaveProperty('description', 'This is a generic action description')
+    expect(action).toHaveProperty('actionTypeName', 'Generic Device Action')
+    expect(action).toHaveProperty('actionTypeUrl', 'https://foo/bar')
+    expect(action.beginDate).toBe(date1)
+    expect(action.endDate).toBe(date2)
+    expect(action.contact).toStrictEqual(contact)
+    expect(action.attachments).toContainEqual(attachment)
+    expect(action.isGenericAction).toBeTruthy()
+  })
+})
diff --git a/test/serializers/jsonapi/ActionTypeSerializer.test.ts b/test/serializers/jsonapi/ActionTypeSerializer.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..2fdcc011494656e25b49e408beae34e11b6c5711
--- /dev/null
+++ b/test/serializers/jsonapi/ActionTypeSerializer.test.ts
@@ -0,0 +1,104 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { ActionType } from '@/models/ActionType'
+import { ActionTypeSerializer } from '@/serializers/jsonapi/ActionTypeSerializer'
+
+describe('ActionTypeSerializer', () => {
+  describe('#convertJsonApiObjectListToModelList', () => {
+    it('should convert a list of two elements to a model list', () => {
+      const jsonApiObjectList: any = {
+        data: [{
+          attributes: {
+            term: 'Configuration Maintenance',
+            definition: 'A configuration (i.e. station) is maintained (i.e. check of functionality). configurationMaintenance actions can be followed by platformMaintenance or deviveMaintenance actions to be explicit.',
+            provenance: null,
+            provenance_uri: null,
+            category: null,
+            note: null,
+            status: 'ACCEPTED'
+          },
+          id: '8',
+          links: {
+            self: 'http://rz-vm64.gfz-potsdam.de:5001/api/v1/actiontypes/8/'
+          },
+          relationships: {},
+          type: 'ActionType'
+        }, {
+          attributes: {
+            term: 'Configuration Observation',
+            definition: 'An observation is made at configuration level (i.e. station is covered by leafs). configurationObservation actions can be followed by platformObservation or deviveObservation actions to be explicit.',
+            provenance: null,
+            provenance_uri: null,
+            category: null,
+            note: null,
+            status: 'ACCEPTED'
+          },
+          id: '9',
+          links: {
+            self: 'http://rz-vm64.gfz-potsdam.de:5001/api/v1/actiontypes/9/'
+          },
+          relationships: {},
+          type: 'ActionType'
+        }],
+        included: [],
+        jsonapi: {
+          version: '1.0'
+        },
+        meta: {
+          count: 2
+        }
+      }
+
+      const expectedActionType1 = ActionType.createFromObject({
+        id: '8',
+        name: 'Configuration Maintenance',
+        definition: 'A configuration (i.e. station) is maintained (i.e. check of functionality). configurationMaintenance actions can be followed by platformMaintenance or deviveMaintenance actions to be explicit.',
+        uri: 'http://rz-vm64.gfz-potsdam.de:5001/api/v1/actiontypes/8/'
+      })
+      const expectedActionType2 = ActionType.createFromObject({
+        id: '9',
+        name: 'Configuration Observation',
+        definition: 'An observation is made at configuration level (i.e. station is covered by leafs). configurationObservation actions can be followed by platformObservation or deviveObservation actions to be explicit.',
+        uri: 'http://rz-vm64.gfz-potsdam.de:5001/api/v1/actiontypes/9/'
+      })
+
+      const serializer = new ActionTypeSerializer()
+
+      const actiontypes = serializer.convertJsonApiObjectListToModelList(jsonApiObjectList)
+
+      expect(Array.isArray(actiontypes)).toBeTruthy()
+      expect(actiontypes.length).toEqual(2)
+      expect(actiontypes[0]).toEqual(expectedActionType1)
+      expect(actiontypes[1]).toEqual(expectedActionType2)
+    })
+  })
+})
diff --git a/test/serializers/jsonapi/GenericActionAttachmentSerializer.test.ts b/test/serializers/jsonapi/GenericActionAttachmentSerializer.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..951aeca4efde212c219eb0fdddd4b7a6e4b3f583
--- /dev/null
+++ b/test/serializers/jsonapi/GenericActionAttachmentSerializer.test.ts
@@ -0,0 +1,314 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { Attachment } from '@/models/Attachment'
+
+import {
+  GenericDeviceActionAttachmentSerializer,
+  GenericPlatformActionAttachmentSerializer
+} from '@/serializers/jsonapi/GenericActionAttachmentSerializer'
+
+import {
+  IJsonApiEntityEnvelope,
+  IJsonApiEntityWithOptionalId,
+  IJsonApiEntityWithOptionalAttributes,
+  IJsonApiRelationships
+} from '@/serializers/jsonapi/JsonApiTypes'
+
+describe('GenericActionAttachmentSerializer', () => {
+  describe('GenericDeviceActionAttachmentSerializer', () => {
+    function getExampleObjectResponse (): IJsonApiEntityEnvelope {
+      return {
+        data: {
+          type: 'generic_device_action',
+          relationships: {
+            generic_device_action_attachments: {
+              links: {
+                related: '/rdm/svm-api/v1/generic-device-actions/9/relationships/generic-device-action-attachments'
+              },
+              data: [
+                {
+                  type: 'generic_device_action_attachment',
+                  id: '6'
+                },
+                {
+                  type: 'generic_device_action_attachment',
+                  id: '7'
+                }
+              ]
+            },
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-actions/9/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            },
+            contact: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-actions/9/relationships/contact',
+                related: '/rdm/svm-api/v1/contacts/14'
+              },
+              data: {
+                type: 'contact',
+                id: '14'
+              }
+            }
+          },
+          attributes: {
+            updated_at: null,
+            created_at: '2021-05-12T08:19:56.781661',
+            action_type_name: 'Device visit',
+            begin_date: '2021-05-21T00:00:00',
+            end_date: '2021-05-30T00:00:00',
+            action_type_uri: '',
+            description: 'dfdfdf'
+          },
+          id: '9',
+          links: {
+            self: '/rdm/svm-api/v1/generic-device-actions/9'
+          }
+        },
+        links: {
+          self: '/rdm/svm-api/v1/generic-device-actions/9'
+        },
+        included: [
+          {
+            type: 'generic_device_action_attachment',
+            relationships: {
+              attachment: {
+                links: {
+                  self: '/rdm/svm-api/v1/generic-device-action-attachments/6/relationships/attachment',
+                  related: '/rdm/svm-api/v1/device-attachments/51'
+                },
+                data: {
+                  type: 'device_attachment',
+                  id: '51'
+                }
+              },
+              action: {
+                links: {
+                  self: '/rdm/svm-api/v1/generic-device-action-attachments/6/relationships/action',
+                  related: '/rdm/svm-api/v1/generic-device-actions/9'
+                }
+              }
+            },
+            id: '6',
+            links: {
+              self: '/rdm/svm-api/v1/generic-device-action-attachments/6'
+            }
+          },
+          {
+            type: 'device_attachment',
+            relationships: {
+              device: {
+                links: {
+                  self: '/rdm/svm-api/v1/device-attachments/51/relationships/device',
+                  related: '/rdm/svm-api/v1/devices/204'
+                },
+                data: {
+                  type: 'device',
+                  id: '204'
+                }
+              }
+            },
+            attributes: {
+              url: 'https://foo.de',
+              label: 'Foo.de'
+            },
+            id: '51',
+            links: {
+              self: '/rdm/svm-api/v1/device-attachments/51'
+            }
+          },
+          {
+            type: 'generic_device_action_attachment',
+            relationships: {
+              attachment: {
+                links: {
+                  self: '/rdm/svm-api/v1/generic-device-action-attachments/7/relationships/attachment',
+                  related: '/rdm/svm-api/v1/device-attachments/52'
+                },
+                data: {
+                  type: 'device_attachment',
+                  id: '52'
+                }
+              },
+              action: {
+                links: {
+                  self: '/rdm/svm-api/v1/generic-device-action-attachments/7/relationships/action',
+                  related: '/rdm/svm-api/v1/generic-device-actions/9'
+                }
+              }
+            },
+            id: '7',
+            links: {
+              self: '/rdm/svm-api/v1/generic-device-action-attachments/7'
+            }
+          },
+          {
+            type: 'device_attachment',
+            relationships: {
+              device: {
+                links: {
+                  self: '/rdm/svm-api/v1/device-attachments/52/relationships/device',
+                  related: '/rdm/svm-api/v1/devices/204'
+                },
+                data: {
+                  type: 'device',
+                  id: '204'
+                }
+              }
+            },
+            attributes: {
+              url: 'https://bar.baz',
+              label: 'Bar.baz'
+            },
+            id: '52',
+            links: {
+              self: '/rdm/svm-api/v1/device-attachments/52'
+            }
+          }
+        ],
+        jsonapi: {
+          version: '1.0'
+        }
+      }
+    }
+
+    describe('constructing and types', () => {
+      it('should return \'device\' as its type', () => {
+        const serializer = new GenericDeviceActionAttachmentSerializer()
+        expect(serializer.targetType).toEqual('device')
+      })
+      it('should return a correct action type name', () => {
+        const serializer = new GenericDeviceActionAttachmentSerializer()
+        expect(serializer.getActionTypeName()).toEqual('generic_device_action')
+      })
+      it('should return a correct action attachment type name', () => {
+        const serializer = new GenericDeviceActionAttachmentSerializer()
+        expect(serializer.getActionAttachmentTypeName()).toEqual('generic_device_action_attachment')
+      })
+      it('should return a the plural form of the action attachment type name', () => {
+        const serializer = new GenericDeviceActionAttachmentSerializer()
+        expect(serializer.getActionAttachmentTypeNamePlural()).toEqual('generic_device_action_attachments')
+      })
+      it('should return a correct attachment type name', () => {
+        const serializer = new GenericDeviceActionAttachmentSerializer()
+        expect(serializer.getAttachmentTypeName()).toEqual('device_attachment')
+      })
+    })
+    describe('#convertModelToJsonApiData', () => {
+      it('should return a JSON API object from an attachment and an action id', () => {
+        const attachment = Attachment.createFromObject({
+          id: '1',
+          label: 'Foo',
+          url: 'https://bar.baz'
+        })
+        const actionId = '2'
+
+        const expectedApiModel: IJsonApiEntityWithOptionalId = {
+          type: 'generic_device_action_attachment',
+          attributes: {},
+          relationships: {
+            action: {
+              data: {
+                type: 'generic_device_action',
+                id: '2'
+              }
+            },
+            attachment: {
+              data: {
+                type: 'device_attachment',
+                id: '1'
+              }
+            }
+          }
+        }
+
+        const serializer = new GenericDeviceActionAttachmentSerializer()
+        const apiModel = serializer.convertModelToJsonApiData(attachment, actionId)
+
+        expect(apiModel).toEqual(expectedApiModel)
+      })
+    })
+    describe('#convertJsonApiRelationshipsModelList', () => {
+      it('should return a serialized list of attachments from an list of included API entities', () => {
+        const attachment1 = Attachment.createFromObject({
+          id: '51',
+          label: 'Foo.de',
+          url: 'https://foo.de'
+        })
+        const attachment2 = Attachment.createFromObject({
+          id: '52',
+          label: 'Bar.baz',
+          url: 'https://bar.baz'
+        })
+
+        const response = getExampleObjectResponse()
+        const serializer = new GenericDeviceActionAttachmentSerializer()
+
+        const attachmentList = serializer.convertJsonApiRelationshipsModelList(response.data.relationships as IJsonApiRelationships, response.included as IJsonApiEntityWithOptionalAttributes[])
+
+        expect(attachmentList).toHaveLength(2)
+        expect(attachmentList).toContainEqual(attachment1)
+        expect(attachmentList).toContainEqual(attachment2)
+      })
+    })
+  })
+  describe('GenericPlatformActionAttachmentSerializer', () => {
+    describe('constructing and types', () => {
+      it('should return \'platform\' as its type', () => {
+        const serializer = new GenericPlatformActionAttachmentSerializer()
+        expect(serializer.targetType).toEqual('platform')
+      })
+      it('should return a correct action type name', () => {
+        const serializer = new GenericPlatformActionAttachmentSerializer()
+        expect(serializer.getActionTypeName()).toEqual('generic_platform_action')
+      })
+      it('should return a correct action attachment type name', () => {
+        const serializer = new GenericPlatformActionAttachmentSerializer()
+        expect(serializer.getActionAttachmentTypeName()).toEqual('generic_platform_action_attachment')
+      })
+      it('should return a the plural form of the action attachment type name', () => {
+        const serializer = new GenericPlatformActionAttachmentSerializer()
+        expect(serializer.getActionAttachmentTypeNamePlural()).toEqual('generic_platform_action_attachments')
+      })
+      it('should return a correct attachment type name', () => {
+        const serializer = new GenericPlatformActionAttachmentSerializer()
+        expect(serializer.getAttachmentTypeName()).toEqual('platform_attachment')
+      })
+    })
+  })
+})
diff --git a/test/serializers/jsonapi/GenericActionSerializer.test.ts b/test/serializers/jsonapi/GenericActionSerializer.test.ts
new file mode 100644
index 0000000000000000000000000000000000000000..44c487d53120d5ebc0fe102a852cf86f4f2e591a
--- /dev/null
+++ b/test/serializers/jsonapi/GenericActionSerializer.test.ts
@@ -0,0 +1,953 @@
+/**
+ * @license
+ * Web client of the Sensor Management System software developed within
+ * the Helmholtz DataHub Initiative by GFZ and UFZ.
+ *
+ * Copyright (C) 2020, 2021
+ * - Nils Brinckmann (GFZ, nils.brinckmann@gfz-potsdam.de)
+ * - Marc Hanisch (GFZ, marc.hanisch@gfz-potsdam.de)
+ * - Helmholtz Centre Potsdam - GFZ German Research Centre for
+ *   Geosciences (GFZ, https://www.gfz-potsdam.de)
+ *
+ * Parts of this program were developed within the context of the
+ * following publicly funded projects or measures:
+ * - Helmholtz Earth and Environment DataHub
+ *   (https://www.helmholtz.de/en/research/earth_and_environment/initiatives/#h51095)
+ *
+ * Licensed under the HEESIL, Version 1.0 or - as soon they will be
+ * approved by the "Community" - subsequent versions of the HEESIL
+ * (the "Licence").
+ *
+ * You may not use this work except in compliance with the Licence.
+ *
+ * You may obtain a copy of the Licence at:
+ * https://gitext.gfz-potsdam.de/software/heesil
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the Licence is distributed on an "AS IS" basis,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ * implied. See the Licence for the specific language governing
+ * permissions and limitations under the Licence.
+ */
+import { DateTime } from 'luxon'
+
+import { Attachment } from '@/models/Attachment'
+import { GenericAction } from '@/models/GenericAction'
+import { Contact } from '@/models/Contact'
+
+import {
+  GenericDeviceActionSerializer,
+  GenericPlatformActionSerializer
+} from '@/serializers/jsonapi/GenericActionSerializer'
+
+import {
+  IJsonApiEntityEnvelope,
+  IJsonApiEntityListEnvelope,
+  IJsonApiEntityWithOptionalId,
+  IJsonApiEntityWithOptionalAttributes,
+  IJsonApiRelationships
+} from '@/serializers/jsonapi/JsonApiTypes'
+
+describe('GenericActionSerializer', () => {
+  function getExampleObjectResponse (): IJsonApiEntityEnvelope {
+    return {
+      data: {
+        type: 'generic_device_action',
+        relationships: {
+          generic_device_action_attachments: {
+            links: {
+              related: '/rdm/svm-api/v1/generic-device-actions/7/relationships/generic-device-action-attachments'
+            },
+            data: [
+              {
+                type: 'generic_device_action_attachment',
+                id: '21'
+              }
+            ]
+          },
+          device: {
+            links: {
+              self: '/rdm/svm-api/v1/generic-device-actions/7/relationships/device',
+              related: '/rdm/svm-api/v1/devices/204'
+            },
+            data: {
+              type: 'device',
+              id: '204'
+            }
+          },
+          contact: {
+            links: {
+              self: '/rdm/svm-api/v1/generic-device-actions/7/relationships/contact',
+              related: '/rdm/svm-api/v1/contacts/14'
+            },
+            data: {
+              type: 'contact',
+              id: '14'
+            }
+          }
+        },
+        attributes: {
+          updated_at: '2021-05-27T07:08:35.964720',
+          created_at: '2021-05-07T09:57:38.203251',
+          action_type_name: 'Device maintainance',
+          begin_date: '2021-05-23T00:00:00',
+          end_date: '2021-06-01T00:00:00',
+          action_type_uri: '',
+          description: 'Bla'
+        },
+        id: '7',
+        links: {
+          self: '/rdm/svm-api/v1/generic-device-actions/7'
+        }
+      },
+      links: {
+        self: '/rdm/svm-api/v1/generic-device-actions/7'
+      },
+      included: [
+        {
+          type: 'contact',
+          relationships: {
+            platforms: {
+              links: {
+                related: '/rdm/svm-api/v1/contacts/14/relationships/platforms'
+              },
+              data: [
+
+              ]
+            },
+            user: {
+              links: {
+                self: '/rdm/svm-api/v1/contacts/14/relationships/user'
+              },
+              data: {
+                type: 'user',
+                id: '6'
+              }
+            },
+            configurations: {
+              links: {
+                related: '/rdm/svm-api/v1/contacts/14/relationships/configurations'
+              },
+              data: [
+
+              ]
+            },
+            devices: {
+              links: {
+                related: '/rdm/svm-api/v1/contacts/14/relationships/devices'
+              },
+              data: [
+                {
+                  type: 'device',
+                  id: '250'
+                }
+              ]
+            }
+          },
+          attributes: {
+            family_name: 'Hanisch',
+            email: 'marc.hanisch@gfz-potsdam.de',
+            website: '',
+            given_name: 'Marc'
+          },
+          id: '14',
+          links: {
+            self: '/rdm/svm-api/v1/contacts/14'
+          }
+        }
+      ],
+      jsonapi: {
+        version: '1.0'
+      }
+    }
+  }
+
+  function getExampleObjectListResponse (): IJsonApiEntityListEnvelope {
+    return {
+      data: [
+        {
+          type: 'generic_device_action',
+          relationships: {
+            generic_device_action_attachments: {
+              links: {
+                related: '/rdm/svm-api/v1/generic-device-actions/4/relationships/generic-device-action-attachments'
+              },
+              data: [
+                {
+                  type: 'generic_device_action_attachment',
+                  id: '1'
+                },
+                {
+                  type: 'generic_device_action_attachment',
+                  id: '2'
+                }
+              ]
+            },
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-actions/4/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            },
+            contact: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-actions/4/relationships/contact',
+                related: '/rdm/svm-api/v1/contacts/14'
+              },
+              data: {
+                type: 'contact',
+                id: '14'
+              }
+            }
+          },
+          attributes: {
+            updated_at: null,
+            created_at: '2021-05-07T09:11:59.289773',
+            action_type_name: 'Device maintainance',
+            begin_date: '2021-05-03T00:00:00',
+            end_date: '2021-05-04T00:00:00',
+            action_type_uri: '',
+            description: 'yet another maintainance'
+          },
+          id: '4',
+          links: {
+            self: '/rdm/svm-api/v1/generic-device-actions/4'
+          }
+        },
+        {
+          type: 'generic_device_action',
+          relationships: {
+            generic_device_action_attachments: {
+              links: {
+                related: '/rdm/svm-api/v1/generic-device-actions/5/relationships/generic-device-action-attachments'
+              },
+              data: [
+                {
+                  type: 'generic_device_action_attachment',
+                  id: '3'
+                }
+              ]
+            },
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-actions/5/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            },
+            contact: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-actions/5/relationships/contact',
+                related: '/rdm/svm-api/v1/contacts/14'
+              },
+              data: {
+                type: 'contact',
+                id: '14'
+              }
+            }
+          },
+          attributes: {
+            updated_at: null,
+            created_at: '2021-05-07T09:23:53.678558',
+            action_type_name: 'Device visit',
+            begin_date: '2021-05-09T00:00:00',
+            end_date: '2021-05-12T00:00:00',
+            action_type_uri: '',
+            description: 'Site was nice!'
+          },
+          id: '5',
+          links: {
+            self: '/rdm/svm-api/v1/generic-device-actions/5'
+          }
+        }
+      ],
+      links: {
+        self: 'http://rz-vm64.gfz-potsdam.de:5000/rdm/svm-api/v1/generic-device-actions?page%5Bsize%5D=10000&include=contact%2Cgeneric_device_action_attachments.attachment'
+      },
+      included: [
+        {
+          type: 'generic_device_action_attachment',
+          relationships: {
+            attachment: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-action-attachments/1/relationships/attachment',
+                related: '/rdm/svm-api/v1/device-attachments/51'
+              },
+              data: {
+                type: 'device_attachment',
+                id: '51'
+              }
+            },
+            action: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-action-attachments/1/relationships/action',
+                related: '/rdm/svm-api/v1/generic-device-actions/4'
+              }
+            }
+          },
+          id: '1',
+          links: {
+            self: '/rdm/svm-api/v1/generic-device-action-attachments/1'
+          }
+        },
+        {
+          type: 'device_attachment',
+          relationships: {
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/device-attachments/51/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            }
+          },
+          attributes: {
+            url: 'https://foo.de',
+            label: 'Foo.de'
+          },
+          id: '51',
+          links: {
+            self: '/rdm/svm-api/v1/device-attachments/51'
+          }
+        },
+        {
+          type: 'generic_device_action_attachment',
+          relationships: {
+            attachment: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-action-attachments/2/relationships/attachment',
+                related: '/rdm/svm-api/v1/device-attachments/52'
+              },
+              data: {
+                type: 'device_attachment',
+                id: '52'
+              }
+            },
+            action: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-action-attachments/2/relationships/action',
+                related: '/rdm/svm-api/v1/generic-device-actions/4'
+              }
+            }
+          },
+          id: '2',
+          links: {
+            self: '/rdm/svm-api/v1/generic-device-action-attachments/2'
+          }
+        },
+        {
+          type: 'device_attachment',
+          relationships: {
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/device-attachments/52/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            }
+          },
+          attributes: {
+            url: 'https://bar.baz',
+            label: 'Bar.baz'
+          },
+          id: '52',
+          links: {
+            self: '/rdm/svm-api/v1/device-attachments/52'
+          }
+        },
+        {
+          type: 'contact',
+          relationships: {
+            user: {
+              links: {
+                self: '/rdm/svm-api/v1/contacts/14/relationships/user'
+              },
+              data: {
+                type: 'user',
+                id: '6'
+              }
+            }
+          },
+          attributes: {
+            family_name: 'Hanisch',
+            email: 'marc.hanisch@gfz-potsdam.de',
+            website: '',
+            given_name: 'Marc'
+          },
+          id: '14',
+          links: {
+            self: '/rdm/svm-api/v1/contacts/14'
+          }
+        },
+        {
+          type: 'generic_device_action_attachment',
+          relationships: {
+            attachment: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-action-attachments/3/relationships/attachment',
+                related: '/rdm/svm-api/v1/device-attachments/52'
+              },
+              data: {
+                type: 'device_attachment',
+                id: '52'
+              }
+            },
+            action: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-action-attachments/3/relationships/action',
+                related: '/rdm/svm-api/v1/generic-device-actions/5'
+              }
+            }
+          },
+          id: '3',
+          links: {
+            self: '/rdm/svm-api/v1/generic-device-action-attachments/3'
+          }
+        }
+      ],
+      meta: {
+        count: 2
+      },
+      jsonapi: {
+        version: '1.0'
+      }
+    }
+  }
+
+  function getExampleDeviceResponse (): IJsonApiEntityEnvelope {
+    return {
+      data: {
+        type: 'device',
+        attributes: {
+          persistent_identifier: null,
+          model: 'SM1',
+          website: 'http://www.adcon.com',
+          short_name: 'Adcon SM1 soil moisture / temperature sensor FTDR Zeitlow 1',
+          serial_number: '',
+          updated_at: '2021-04-26T09:03:01.944689',
+          long_name: 'Adcon SM 1 soil moisture / temperature sensor',
+          device_type_uri: '',
+          status_name: 'In Use',
+          dual_use: false,
+          device_type_name: 'Frequency/Time Domain Reflectometer (FTDR)(Soil moisture and temperature)',
+          description: '',
+          inventory_number: '',
+          manufacturer_name: 'OTT Hydromet GmbH',
+          created_at: '2021-01-18T07:07:24.360000',
+          manufacturer_uri: 'OTT Hydromet GmbH',
+          status_uri: 'http://rz-vm64.gfz-potsdam.de:8000/api/v1/equipmentstatus/2/'
+        },
+        relationships: {
+          generic_device_actions: {
+            links: {
+              related: '/rdm/svm-api/v1/devices/204/relationships/generic-device-actions'
+            },
+            data: [
+              {
+                type: 'generic_device_action',
+                id: '3'
+              },
+              {
+                type: 'generic_device_action',
+                id: '4'
+              }
+            ]
+          }
+        },
+        id: '204',
+        links: {
+          self: '/rdm/svm-api/v1/devices/204'
+        }
+      },
+      links: {
+        self: '/rdm/svm-api/v1/devices/204'
+      },
+      included: [
+        {
+          type: 'generic_device_action',
+          relationships: {
+            generic_device_action_attachments: {
+              links: {
+                related: '/rdm/svm-api/v1/generic-device-actions/3/relationships/generic-device-action-attachments'
+              },
+              data: [
+
+              ]
+            },
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-actions/3/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            },
+            contact: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-actions/3/relationships/contact',
+                related: '/rdm/svm-api/v1/contacts/14'
+              },
+              data: {
+                type: 'contact',
+                id: '14'
+              }
+            }
+          },
+          attributes: {
+            updated_at: null,
+            created_at: '2021-05-06T12:54:18.899177',
+            action_type_name: 'Device maintainance',
+            begin_date: '2021-05-01T00:00:00',
+            end_date: '2021-05-03T00:00:00',
+            action_type_uri: '',
+            description: 'Rasenmähen'
+          },
+          id: '3',
+          links: {
+            self: '/rdm/svm-api/v1/generic-device-actions/3'
+          }
+        },
+        {
+          type: 'generic_device_action',
+          relationships: {
+            generic_device_action_attachments: {
+              links: {
+                related: '/rdm/svm-api/v1/generic-device-actions/4/relationships/generic-device-action-attachments'
+              },
+              data: [
+                {
+                  type: 'generic_device_action_attachment',
+                  id: '1'
+                },
+                {
+                  type: 'generic_device_action_attachment',
+                  id: '2'
+                }
+              ]
+            },
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-actions/4/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            },
+            contact: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-actions/4/relationships/contact',
+                related: '/rdm/svm-api/v1/contacts/14'
+              },
+              data: {
+                type: 'contact',
+                id: '14'
+              }
+            }
+          },
+          attributes: {
+            updated_at: null,
+            created_at: '2021-05-07T09:11:59.289773',
+            action_type_name: 'Device maintainance',
+            begin_date: '2021-05-03T00:00:00',
+            end_date: '2021-05-04T00:00:00',
+            action_type_uri: '',
+            description: 'yet another maintainance'
+          },
+          id: '4',
+          links: {
+            self: '/rdm/svm-api/v1/generic-device-actions/4'
+          }
+        }
+      ],
+      jsonapi: {
+        version: '1.0'
+      }
+    }
+  }
+
+  function getExampleObjectResponseWithIncludedActionAttachments (): IJsonApiEntityEnvelope {
+    return {
+      data: {
+        type: 'generic_device_action',
+        relationships: {
+          generic_device_action_attachments: {
+            links: {
+              related: '/rdm/svm-api/v1/generic-device-actions/7/relationships/generic-device-action-attachments'
+            },
+            data: [
+              {
+                type: 'generic_device_action_attachment',
+                id: '21'
+              }
+            ]
+          },
+          device: {
+            links: {
+              self: '/rdm/svm-api/v1/generic-device-actions/7/relationships/device',
+              related: '/rdm/svm-api/v1/devices/204'
+            },
+            data: {
+              type: 'device',
+              id: '204'
+            }
+          },
+          contact: {
+            links: {
+              self: '/rdm/svm-api/v1/generic-device-actions/7/relationships/contact',
+              related: '/rdm/svm-api/v1/contacts/14'
+            },
+            data: {
+              type: 'contact',
+              id: '14'
+            }
+          }
+        },
+        attributes: {
+          updated_at: '2021-05-28T11:12:54.938479',
+          created_at: '2021-05-07T09:57:38.203251',
+          action_type_name: 'Device maintainance',
+          begin_date: '2021-05-23T00:00:00',
+          end_date: '2021-06-01T00:00:00',
+          action_type_uri: '',
+          description: 'Bla'
+        },
+        id: '7',
+        links: {
+          self: '/rdm/svm-api/v1/generic-device-actions/7'
+        }
+      },
+      links: {
+        self: '/rdm/svm-api/v1/generic-device-actions/7'
+      },
+      included: [
+        {
+          type: 'generic_device_action_attachment',
+          relationships: {
+            attachment: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-action-attachments/21/relationships/attachment',
+                related: '/rdm/svm-api/v1/device-attachments/51'
+              },
+              data: {
+                type: 'device_attachment',
+                id: '51'
+              }
+            },
+            action: {
+              links: {
+                self: '/rdm/svm-api/v1/generic-device-action-attachments/21/relationships/action',
+                related: '/rdm/svm-api/v1/generic-device-actions/7'
+              }
+            }
+          },
+          id: '21',
+          links: {
+            self: '/rdm/svm-api/v1/generic-device-action-attachments/21'
+          }
+        },
+        {
+          type: 'device_attachment',
+          relationships: {
+            device: {
+              links: {
+                self: '/rdm/svm-api/v1/device-attachments/51/relationships/device',
+                related: '/rdm/svm-api/v1/devices/204'
+              },
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            }
+          },
+          attributes: {
+            url: 'https://foo.de',
+            label: 'Foo.de'
+          },
+          id: '51',
+          links: {
+            self: '/rdm/svm-api/v1/device-attachments/51'
+          }
+        }
+      ],
+      jsonapi: {
+        version: '1.0'
+      }
+    }
+  }
+
+  describe('GenericDeviceActionSerializer', () => {
+    describe('constructing and types', () => {
+      it('should return \'device\' as its type', () => {
+        const serializer = new GenericDeviceActionSerializer()
+        expect(serializer.targetType).toEqual('device')
+      })
+      it('should return a correct action type name', () => {
+        const serializer = new GenericDeviceActionSerializer()
+        expect(serializer.getActionTypeName()).toEqual('generic_device_action')
+      })
+      it('should return a the plural form of the action type name', () => {
+        const serializer = new GenericDeviceActionSerializer()
+        expect(serializer.getActionTypeNamePlural()).toEqual('generic_device_actions')
+      })
+      it('should return a correction action attachment type name', () => {
+        const serializer = new GenericDeviceActionSerializer()
+        expect(serializer.getActionAttachmentTypeName()).toEqual('generic_device_action_attachment')
+      })
+    })
+    describe('#convertJsonApiObjectToModel', () => {
+      it('should return a serialized generic action from an API response', () => {
+        const contact = Contact.createFromObject({
+          id: '14',
+          givenName: 'Marc',
+          familyName: 'Hanisch',
+          email: 'marc.hanisch@gfz-potsdam.de',
+          website: ''
+        })
+        const expectedAction = new GenericAction()
+        expectedAction.id = '7'
+        expectedAction.description = 'Bla'
+        expectedAction.actionTypeName = 'Device maintainance'
+        expectedAction.actionTypeUrl = ''
+        expectedAction.beginDate = DateTime.fromISO('2021-05-23T00:00:00', { zone: 'UTC' })
+        expectedAction.endDate = DateTime.fromISO('2021-06-01T00:00:00', { zone: 'UTC' })
+        expectedAction.contact = contact
+
+        const serializer = new GenericDeviceActionSerializer()
+        const action = serializer.convertJsonApiObjectToModel(getExampleObjectResponse())
+
+        expect(action).toEqual(expectedAction)
+      })
+    })
+    describe('#convertJsonApiDataToModel', () => {
+      it('should return a serialized generic action from an API response object', () => {
+        const contact = Contact.createFromObject({
+          id: '14',
+          givenName: 'Marc',
+          familyName: 'Hanisch',
+          email: 'marc.hanisch@gfz-potsdam.de',
+          website: ''
+        })
+        const expectedAction = new GenericAction()
+        expectedAction.id = '7'
+        expectedAction.description = 'Bla'
+        expectedAction.actionTypeName = 'Device maintainance'
+        expectedAction.actionTypeUrl = ''
+        expectedAction.beginDate = DateTime.fromISO('2021-05-23T00:00:00', { zone: 'UTC' })
+        expectedAction.endDate = DateTime.fromISO('2021-06-01T00:00:00', { zone: 'UTC' })
+        expectedAction.contact = contact
+
+        const serializer = new GenericDeviceActionSerializer()
+        const data = getExampleObjectResponse().data
+        const included = getExampleObjectResponse().included
+        const action = serializer.convertJsonApiDataToModel(data, included as IJsonApiEntityWithOptionalAttributes[])
+
+        expect(action).toEqual(expectedAction)
+      })
+    })
+    describe('#convertJsonApiRelationshipsModelList', () => {
+      it('should return a serialized list of generic actions from an list of included API entities', () => {
+        const serializer = new GenericDeviceActionSerializer()
+        const response = getExampleDeviceResponse()
+
+        const relationships = response.data.relationships
+        const included = response.included
+
+        const expectedAction1 = new GenericAction()
+        expectedAction1.id = '3'
+        expectedAction1.description = 'Rasenmähen'
+        expectedAction1.actionTypeName = 'Device maintainance'
+        expectedAction1.actionTypeUrl = ''
+        expectedAction1.beginDate = DateTime.fromISO('2021-05-01T00:00:00', { zone: 'UTC' })
+        expectedAction1.endDate = DateTime.fromISO('2021-05-03T00:00:00', { zone: 'UTC' })
+
+        const expectedAction2 = new GenericAction()
+        expectedAction2.id = '4'
+        expectedAction2.description = 'yet another maintainance'
+        expectedAction2.actionTypeName = 'Device maintainance'
+        expectedAction2.actionTypeUrl = ''
+        expectedAction2.beginDate = DateTime.fromISO('2021-05-03T00:00:00', { zone: 'UTC' })
+        expectedAction2.endDate = DateTime.fromISO('2021-05-04T00:00:00', { zone: 'UTC' })
+
+        const actionList = serializer.convertJsonApiRelationshipsModelList(relationships as IJsonApiRelationships, included as IJsonApiEntityWithOptionalAttributes[])
+
+        expect(actionList).toHaveProperty('genericDeviceActions')
+        expect(actionList.genericDeviceActions).toContainEqual(expectedAction1)
+        expect(actionList.genericDeviceActions).toContainEqual(expectedAction2)
+      })
+    })
+    describe('#convertJsonApiObjectListToModelList', () => {
+      it('should return a list of serialized generic action from an API response', () => {
+        const contact = Contact.createFromObject({
+          id: '14',
+          givenName: 'Marc',
+          familyName: 'Hanisch',
+          email: 'marc.hanisch@gfz-potsdam.de',
+          website: ''
+        })
+        const attachment1 = Attachment.createFromObject({
+          id: '51',
+          label: 'Foo.de',
+          url: 'https://foo.de'
+        })
+        const attachment2 = Attachment.createFromObject({
+          id: '52',
+          label: 'Bar.baz',
+          url: 'https://bar.baz'
+        })
+        const expectedAction1 = new GenericAction()
+        expectedAction1.id = '4'
+        expectedAction1.description = 'yet another maintainance'
+        expectedAction1.actionTypeName = 'Device maintainance'
+        expectedAction1.actionTypeUrl = ''
+        expectedAction1.beginDate = DateTime.fromISO('2021-05-03T00:00:00', { zone: 'UTC' })
+        expectedAction1.endDate = DateTime.fromISO('2021-05-04T00:00:00', { zone: 'UTC' })
+        expectedAction1.contact = contact
+        expectedAction1.attachments = [
+          attachment1,
+          attachment2
+        ]
+
+        const expectedAction2 = new GenericAction()
+        expectedAction2.id = '5'
+        expectedAction2.description = 'Site was nice!'
+        expectedAction2.actionTypeName = 'Device visit'
+        expectedAction2.actionTypeUrl = ''
+        expectedAction2.beginDate = DateTime.fromISO('2021-05-09T00:00:00', { zone: 'UTC' })
+        expectedAction2.endDate = DateTime.fromISO('2021-05-12T00:00:00', { zone: 'UTC' })
+        expectedAction2.contact = contact
+        expectedAction2.attachments = [
+          attachment2
+        ]
+
+        const serializer = new GenericDeviceActionSerializer()
+        const actionList = serializer.convertJsonApiObjectListToModelList(getExampleObjectListResponse())
+
+        expect(actionList).toContainEqual(expectedAction1)
+        expect(actionList).toContainEqual(expectedAction2)
+      })
+    })
+    describe('#convertModelToJsonApiData', () => {
+      it('should return a JSON API representation from a generic action model', () => {
+        const contact = Contact.createFromObject({
+          id: '14',
+          givenName: 'Marc',
+          familyName: 'Hanisch',
+          email: 'marc.hanisch@gfz-potsdam.de',
+          website: ''
+        })
+
+        const action = new GenericAction()
+        action.id = '7'
+        action.description = 'Bla'
+        action.actionTypeName = 'Device maintainance'
+        action.actionTypeUrl = ''
+        action.beginDate = DateTime.fromISO('2021-05-23T00:00:00', { zone: 'UTC' })
+        action.endDate = DateTime.fromISO('2021-06-01T00:00:00', { zone: 'UTC' })
+        action.contact = contact
+
+        const expectedApiModel: IJsonApiEntityWithOptionalId = {
+          type: 'generic_device_action',
+          id: '7',
+          attributes: {
+            description: 'Bla',
+            action_type_name: 'Device maintainance',
+            action_type_uri: '',
+            begin_date: '2021-05-23T00:00:00.000Z',
+            end_date: '2021-06-01T00:00:00.000Z'
+          },
+          relationships: {
+            device: {
+              data: {
+                type: 'device',
+                id: '204'
+              }
+            },
+            contact: {
+              data: {
+                type: 'contact',
+                id: '14'
+              }
+            }
+          }
+        }
+
+        const serializer = new GenericDeviceActionSerializer()
+        const apiModel = serializer.convertModelToJsonApiData(action, '204')
+
+        expect(apiModel).toEqual(expectedApiModel)
+      })
+    })
+    describe('#convertModelToJsonApiRelationshipObject', () => {
+      it('should return a JSON API relationships object from a generic action model', () => {
+        const action = new GenericAction()
+        action.id = '7'
+
+        const expectedRelationship: IJsonApiRelationships = {
+          generic_device_action: {
+            data: {
+              id: '7',
+              type: 'generic_device_action'
+            }
+          }
+        }
+        const serializer = new GenericDeviceActionSerializer()
+        const apiRelationship = serializer.convertModelToJsonApiRelationshipObject(action)
+
+        expect(apiRelationship).toEqual(expectedRelationship)
+      })
+    })
+    describe('#convertJsonApiIncludedGenericActionAttachmentsToIdList', () => {
+      it('should return a list of generic_device_action_attachment ids / attachment ids mappings', () => {
+        const expectedMappings = [
+          {
+            genericActionAttachmentId: '21',
+            attachmentId: '51'
+          }
+        ]
+        const serializer = new GenericDeviceActionSerializer()
+        const data = getExampleObjectResponseWithIncludedActionAttachments()
+        const mappings = serializer.convertJsonApiIncludedGenericActionAttachmentsToIdList(data.included as IJsonApiEntityWithOptionalAttributes[])
+
+        expect(mappings).toEqual(expectedMappings)
+      })
+    })
+  })
+
+  describe('GenericPlatformActionSerializer', () => {
+    describe('constructing and types', () => {
+      it('should return \'platform\' as its type', () => {
+        const serializer = new GenericPlatformActionSerializer()
+        expect(serializer.targetType).toEqual('platform')
+      })
+      it('should return a correct action type name', () => {
+        const serializer = new GenericPlatformActionSerializer()
+        expect(serializer.getActionTypeName()).toEqual('generic_platform_action')
+      })
+      it('should return a the plural form of the action type name', () => {
+        const serializer = new GenericPlatformActionSerializer()
+        expect(serializer.getActionTypeNamePlural()).toEqual('generic_platform_actions')
+      })
+      it('should return a correction action attachment type name', () => {
+        const serializer = new GenericPlatformActionSerializer()
+        expect(serializer.getActionAttachmentTypeName()).toEqual('generic_platform_action_attachment')
+      })
+    })
+  })
+})
diff --git a/utils/dateHelper.ts b/utils/dateHelper.ts
index 832dc9a105df55b70276b4533c20e9567d71b8ca..35d73f356a714b8b5286d294a19816fc18c7a34f 100644
--- a/utils/dateHelper.ts
+++ b/utils/dateHelper.ts
@@ -50,3 +50,10 @@ export const timeStampToUTCDateTime = (value: number) : string => {
   const date = DateTime.fromSeconds(value).setZone('UTC')
   return date.toFormat('yyyy-MM-dd HH:mm:ss') + ' UTC'
 }
+
+export const dateToDateTimeString = (aDate: DateTime | null): string => {
+  if (!aDate) {
+    return ''
+  }
+  return aDate.setZone('UTC').toFormat('yyyy-MM-dd HH:mm:ss')
+}