diff --git a/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala b/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala
index 4792a4d9..8f6179d0 100644
--- a/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala
+++ b/modules/backend/src/main/scala/docspell/backend/ops/OCustomFields.scala
@@ -1,6 +1,7 @@
 package docspell.backend.ops
 
 import cats.data.EitherT
+import cats.data.NonEmptyList
 import cats.data.OptionT
 import cats.effect._
 import cats.implicits._
@@ -20,6 +21,7 @@ import docspell.store.records.RCustomFieldValue
 import docspell.store.records.RItem
 
 import doobie._
+import org.log4s.getLogger
 
 trait OCustomFields[F[_]] {
 
@@ -41,6 +43,8 @@ trait OCustomFields[F[_]] {
   /** Sets a value given a field an an item. Existing values are overwritten. */
   def setValue(item: Ident, value: SetValue): F[SetValueResult]
 
+  def setValueMultiple(items: NonEmptyList[Ident], value: SetValue): F[SetValueResult]
+
   /** Deletes a value for a given field an item. */
   def deleteValue(in: RemoveValue): F[UpdateResult]
 }
@@ -79,7 +83,7 @@ object OCustomFields {
 
   case class RemoveValue(
       field: Ident,
-      item: Ident,
+      item: NonEmptyList[Ident],
       collective: Ident
   )
 
@@ -88,8 +92,10 @@ object OCustomFields {
   ): Resource[F, OCustomFields[F]] =
     Resource.pure[F, OCustomFields[F]](new OCustomFields[F] {
 
+      private[this] val logger = Logger.log4s[ConnectionIO](getLogger)
+
       def findAll(coll: Ident, nameQuery: Option[String]): F[Vector[CustomFieldData]] =
-        store.transact(QCustomField.findAllLike(coll, nameQuery))
+        store.transact(QCustomField.findAllLike(coll, nameQuery.filter(_.nonEmpty)))
 
       def findById(coll: Ident, field: Ident): F[Option[CustomFieldData]] =
         store.transact(QCustomField.findById(field, coll))
@@ -113,6 +119,7 @@ object OCustomFields {
         val update =
           for {
             field <- OptionT(RCustomField.findByIdOrName(fieldIdOrName, coll))
+            _     <- OptionT.liftF(logger.info(s"Deleting field: $field"))
             n     <- OptionT.liftF(RCustomFieldValue.deleteByField(field.id))
             k     <- OptionT.liftF(RCustomField.deleteById(field.id, coll))
           } yield n + k
@@ -121,24 +128,32 @@ object OCustomFields {
       }
 
       def setValue(item: Ident, value: SetValue): F[SetValueResult] =
+        setValueMultiple(NonEmptyList.of(item), value)
+
+      def setValueMultiple(
+          items: NonEmptyList[Ident],
+          value: SetValue
+      ): F[SetValueResult] =
         (for {
           field <- EitherT.fromOptionF(
             store.transact(RCustomField.findByIdOrName(value.field, value.collective)),
             SetValueResult.fieldNotFound
           )
-          _ <- EitherT(
-            store
-              .transact(RItem.existsByIdAndCollective(item, value.collective))
-              .map(flag => if (flag) Right(()) else Left(SetValueResult.itemNotFound))
-          )
           fval <- EitherT.fromEither[F](
             field.ftype
               .parseValue(value.value)
               .leftMap(SetValueResult.valueInvalid)
               .map(field.ftype.valueString)
           )
+          _ <- EitherT(
+            store
+              .transact(RItem.existsByIdsAndCollective(items, value.collective))
+              .map(flag => if (flag) Right(()) else Left(SetValueResult.itemNotFound))
+          )
           nu <- EitherT.right[SetValueResult](
-            store.transact(RCustomField.setValue(field, item, fval))
+            items
+              .traverse(item => store.transact(RCustomField.setValue(field, item, fval)))
+              .map(_.toList.sum)
           )
         } yield nu).fold(identity, _ => SetValueResult.success)
 
diff --git a/modules/restapi/src/main/resources/docspell-openapi.yml b/modules/restapi/src/main/resources/docspell-openapi.yml
index c1a849b1..8774a2f7 100644
--- a/modules/restapi/src/main/resources/docspell-openapi.yml
+++ b/modules/restapi/src/main/resources/docspell-openapi.yml
@@ -2383,6 +2383,52 @@ paths:
               schema:
                 $ref: "#/components/schemas/BasicResult"
 
+  /sec/items/customfield:
+    put:
+      tags: [ Item (Multi Edit) ]
+      summary: Set the value of a custom field for multiple items
+      description: |
+        Sets the value for a custom field to multiple given items. If
+        a value already exists, it is overwritten.
+      security:
+        - authTokenHeader: []
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: "#/components/schemas/ItemsAndFieldValue"
+      responses:
+        200:
+          description: Ok
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/BasicResult"
+
+  /sec/items/customfieldremove:
+    post:
+      tags: [ Item (Multi Edit) ]
+      summary: Removes the value for a custom field on multiple items
+      description: |
+        Removes the value for the given custom field from multiple
+        items. The field may be specified by its id or name.
+      security:
+        - authTokenHeader: []
+      parameters:
+        - $ref: "#/components/parameters/itemId"
+      requestBody:
+        content:
+          application/json:
+            schema:
+              $ref: "#/components/schemas/ItemsAndName"
+      responses:
+        200:
+          description: Ok
+          content:
+            application/json:
+              schema:
+                $ref: "#/components/schemas/BasicResult"
+
 
   /sec/attachment/{id}:
     delete:
@@ -3246,7 +3292,7 @@ paths:
               schema:
                 $ref: "#/components/schemas/BasicResult"
 
-  /sec/customfields:
+  /sec/customfield:
     get:
       tags: [ Custom Fields ]
       summary: Get all defined custom fields.
@@ -3283,7 +3329,7 @@ paths:
               schema:
                 $ref: "#/components/schemas/BasicResult"
 
-  /sec/customfields/{id}:
+  /sec/customfield/{id}:
     parameters:
       - $ref: "#/components/parameters/id"
     get:
@@ -3342,6 +3388,21 @@ paths:
 
 components:
   schemas:
+    ItemsAndFieldValue:
+      description: |
+        Holds a list of item ids and a custom field value.
+      required:
+        - items
+        - field
+      properties:
+        items:
+          type: array
+          items:
+            type: string
+            format: ident
+        field:
+          $ref: "#/components/schemas/CustomFieldValue"
+
     ItemsAndRefs:
       description: |
         Holds a list of item ids and a list of ids of some other
@@ -3459,6 +3520,12 @@ components:
         ftype:
           type: string
           format: customfieldtype
+          enum:
+            - text
+            - numeric
+            - date
+            - bool
+            - money
 
     CustomField:
       description: |
@@ -3481,6 +3548,12 @@ components:
         ftype:
           type: string
           format: customfieldtype
+          enum:
+            - text
+            - numeric
+            - date
+            - bool
+            - money
         usages:
           type: integer
           format: int32
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala
index ceab2aea..0aab3c17 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/CustomFieldRoutes.scala
@@ -56,7 +56,7 @@ object CustomFieldRoutes {
 
       case DELETE -> Root / Ident(id) =>
         for {
-          res  <- backend.customFields.delete(id, user.account.collective)
+          res  <- backend.customFields.delete(user.account.collective, id)
           resp <- Ok(convertResult(res))
         } yield resp
     }
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala
index 7b1dd931..4b15689c 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemMultiRoutes.scala
@@ -8,6 +8,7 @@ import cats.implicits._
 
 import docspell.backend.BackendApp
 import docspell.backend.auth.AuthToken
+import docspell.backend.ops.OCustomFields.{RemoveValue, SetValue}
 import docspell.common.{Ident, ItemState}
 import docspell.restapi.model._
 import docspell.restserver.conv.Conversions
@@ -180,6 +181,29 @@ object ItemMultiRoutes {
           )
           resp <- Ok(res)
         } yield resp
+
+      case req @ PUT -> Root / "customfield" =>
+        for {
+          json  <- req.as[ItemsAndFieldValue]
+          items <- readIds[F](json.items)
+          res <- backend.customFields.setValueMultiple(
+            items,
+            SetValue(json.field.field, json.field.value, user.account.collective)
+          )
+          resp <- Ok(Conversions.basicResult(res))
+        } yield resp
+
+      case req @ POST -> Root / "customfieldremove" =>
+        for {
+          json  <- req.as[ItemsAndName]
+          items <- readIds[F](json.items)
+          field <- readId[F](json.name)
+          res <- backend.customFields.deleteValue(
+            RemoveValue(field, items, user.account.collective)
+          )
+          resp <- Ok(Conversions.basicResult(res, "Custom fields removed."))
+        } yield resp
+
     }
   }
 
diff --git a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala
index 8f25eb08..c6606667 100644
--- a/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala
+++ b/modules/restserver/src/main/scala/docspell/restserver/routes/ItemRoutes.scala
@@ -372,7 +372,7 @@ object ItemRoutes {
       case DELETE -> Root / Ident(id) / "customfield" / Ident(fieldId) =>
         for {
           res <- backend.customFields.deleteValue(
-            RemoveValue(fieldId, id, user.account.collective)
+            RemoveValue(fieldId, NonEmptyList.of(id), user.account.collective)
           )
           resp <- Ok(Conversions.basicResult(res, "Custom field value removed."))
         } yield resp
diff --git a/modules/store/src/main/scala/docspell/store/records/RCustomFieldValue.scala b/modules/store/src/main/scala/docspell/store/records/RCustomFieldValue.scala
index 7789143c..53356233 100644
--- a/modules/store/src/main/scala/docspell/store/records/RCustomFieldValue.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RCustomFieldValue.scala
@@ -1,5 +1,7 @@
 package docspell.store.records
 
+import cats.data.NonEmptyList
+
 import docspell.common._
 import docspell.store.impl.Column
 import docspell.store.impl.Implicits._
@@ -57,6 +59,6 @@ object RCustomFieldValue {
   def deleteByItem(item: Ident): ConnectionIO[Int] =
     deleteFrom(table, Columns.itemId.is(item)).update.run
 
-  def deleteValue(fieldId: Ident, item: Ident): ConnectionIO[Int] =
-    deleteFrom(table, and(Columns.id.is(fieldId), Columns.itemId.is(item))).update.run
+  def deleteValue(fieldId: Ident, items: NonEmptyList[Ident]): ConnectionIO[Int] =
+    deleteFrom(table, and(Columns.id.is(fieldId), Columns.itemId.isIn(items))).update.run
 }
diff --git a/modules/store/src/main/scala/docspell/store/records/RItem.scala b/modules/store/src/main/scala/docspell/store/records/RItem.scala
index 89f78e2a..980cd324 100644
--- a/modules/store/src/main/scala/docspell/store/records/RItem.scala
+++ b/modules/store/src/main/scala/docspell/store/records/RItem.scala
@@ -326,6 +326,15 @@ object RItem {
   def existsByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Boolean] =
     selectCount(id, table, and(id.is(itemId), cid.is(coll))).query[Int].unique.map(_ > 0)
 
+  def existsByIdsAndCollective(
+      itemIds: NonEmptyList[Ident],
+      coll: Ident
+  ): ConnectionIO[Boolean] =
+    selectCount(id, table, and(id.isIn(itemIds), cid.is(coll)))
+      .query[Int]
+      .unique
+      .map(_ == itemIds.size)
+
   def findByIdAndCollective(itemId: Ident, coll: Ident): ConnectionIO[Option[RItem]] =
     selectSimple(all, table, and(id.is(itemId), cid.is(coll))).query[RItem].option