From 2821cc27d782beace055f3b55ae41e7246fd3736 Mon Sep 17 00:00:00 2001
From: palajsamuel <palaj@netgrif.com>
Date: Thu, 23 Apr 2026 13:30:53 +0200
Subject: [PATCH 1/8] [NAE-2390] Action API Improvements

- implemented new action API method to ActionDelegate
- added getOption method to MapOptionsField
- tests for new functionality added to ActionDelegateTest
- test xml NAE-2390_action_api_improvements.xml added
---
 .../domain/dataset/MapOptionsField.groovy     |   4 +
 .../logic/action/ActionDelegate.groovy        | 589 +++++++++++++++++-
 .../engine/action/ActionDelegateTest.groovy   | 351 ++++++++++-
 .../NAE-2390_action_api_improvements.xml      | 214 +++++++
 4 files changed, 1134 insertions(+), 24 deletions(-)
 create mode 100644 src/test/resources/petriNets/NAE-2390_action_api_improvements.xml

diff --git a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/MapOptionsField.groovy b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/MapOptionsField.groovy
index 1a26e8db413..f0acc8e978a 100644
--- a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/MapOptionsField.groovy
+++ b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/MapOptionsField.groovy
@@ -40,4 +40,8 @@ abstract class MapOptionsField<T, U> extends Field<U> {
     boolean isDynamic() {
         return this.optionsExpression != null
     }
+
+    T getOption(String key) {
+        return this.options.get(key)
+    }
 }
diff --git a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
index 26791594b3e..69391895d78 100644
--- a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
+++ b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
@@ -54,6 +54,7 @@ import com.netgrif.application.engine.startup.FilterRunner
 import com.netgrif.application.engine.startup.ImportHelper
 import com.netgrif.application.engine.utils.FullPageRequest
 import com.netgrif.application.engine.workflow.domain.Case
+import com.netgrif.application.engine.workflow.domain.DataField
 import com.netgrif.application.engine.workflow.domain.QCase
 import com.netgrif.application.engine.workflow.domain.QTask
 import com.netgrif.application.engine.workflow.domain.Task
@@ -62,6 +63,8 @@ import com.netgrif.application.engine.workflow.domain.eventoutcomes.caseoutcomes
 import com.netgrif.application.engine.workflow.domain.eventoutcomes.dataoutcomes.GetDataEventOutcome
 import com.netgrif.application.engine.workflow.domain.eventoutcomes.dataoutcomes.SetDataEventOutcome
 import com.netgrif.application.engine.workflow.domain.eventoutcomes.taskoutcomes.AssignTaskEventOutcome
+import com.netgrif.application.engine.workflow.domain.eventoutcomes.taskoutcomes.CancelTaskEventOutcome
+import com.netgrif.application.engine.workflow.domain.eventoutcomes.taskoutcomes.FinishTaskEventOutcome
 import com.netgrif.application.engine.workflow.domain.eventoutcomes.taskoutcomes.TaskEventOutcome
 import com.netgrif.application.engine.workflow.service.FileFieldInputStream
 import com.netgrif.application.engine.workflow.service.TaskService
@@ -915,11 +918,194 @@ class ActionDelegate {
         return result.content
     }
 
+    /**
+     * Finds cases referenced by a field in its value.
+     *
+     * Use this overload when working on a case from the current action context. For working with fields from out of the
+     * current action context see other overloads of this action.
+     *
+     * <p>If the field value is {@code null}, this method returns an empty list.</p>
+     * <p>If the value cannot be converted to case IDs, this method returns {@code null}.</p>
+     *
+     * @param caseRef field whose value contains case IDs, may be of types
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#CASE_REF},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#ENUMERATION_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#STRING_COLLECTION},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TEXT},
+     * @return list of matching cases, or an empty list when the field value is {@code null}
+     * @see ActionDelegate#findCases(DataField)
+     * @see ActionDelegate#findCases(List)
+     * @see ActionDelegate#findCases(Closure)
+     * @see ActionDelegate#findCases(Closure, Pageable)
+     */
+    List<Case> findCases(Field caseRef) {
+        if(caseRef.value == null) {
+            log.error("Value of field with id [${caseRef.importId}] is null, returning empty list.")
+            return []
+        }
+        try {
+            return this.findCases([caseRef.value].flatten() as List<String>)
+        } catch (ClassCastException e) {
+            log.error("Method cannot be used with field with id [${caseRef.importId}].", e)
+            return null
+        }
+    }
+
+    /**
+     * Finds cases referenced by a dataField in its value.
+     *
+     * Use this overload when working on a case not from the current action context. For working with fields from the current
+     * action context see other overloads of this action.
+     *
+     * <p>If the field value is {@code null}, this method returns an empty list.</p>
+     * <p>If the value cannot be converted to case IDs, this method returns {@code null}.</p>
+     *
+     * @param caseRef field whose value contains case IDs, may be of types
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#CASE_REF},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#ENUMERATION_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#STRING_COLLECTION},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TEXT},
+     * @return list of matching cases, or an empty list when the field value is {@code null}
+     * @see ActionDelegate#findCases(Field)
+     * @see ActionDelegate#findCases(List)
+     * @see ActionDelegate#findCases(Closure)
+     * @see ActionDelegate#findCases(Closure, Pageable)
+     */
+    List<Case> findCases(DataField caseRef) {
+        if(caseRef.value == null) {
+            log.error("Value of field is null, returning empty list.")
+            return []
+        }
+        try {
+            return this.findCases([caseRef.value].flatten() as List<String>)
+        } catch (ClassCastException e) {
+            log.error("Method cannot be used with field.", e)
+            return null
+        }
+    }
+
+
+    /**
+     * Finds cases by their MongoDB IDs.
+     *
+     * @param mongoIds list of case IDs
+     * @return list of matching cases, or an empty list when the input is {@code null}
+     * @see ActionDelegate#findCases(Field)
+     * @see ActionDelegate#findCases(DataField)
+     * @see ActionDelegate#findCases(Closure)
+     * @see ActionDelegate#findCases(Closure, Pageable)
+     */
+    List<Case> findCases(List<String> mongoIds) {
+        if(mongoIds == null) {
+            log.warn("Null value detected, returning empty list.")
+            return []
+        }
+        return workflowService.findAllById(mongoIds)
+    }
+
     Case findCase(Closure<Predicate> predicate) {
         QCase qCase = new QCase("case")
         return workflowService.searchOne(predicate(qCase))
     }
 
+
+
+    /**
+     * Finds the first case referenced by a field in its value.
+     *
+     * Use this overload when working on a case from current action context. For working with fields from out of the
+     * current action context see other overloads of this action.
+     *
+     * <p>If the field value is {@code null}, this method returns an empty list.</p>
+     * <p>If the value cannot be converted to case IDs, this method returns {@code null}.</p>
+     *
+     * @param caseRef field whose value contains case IDs, may be of types
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#CASE_REF},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#ENUMERATION_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#STRING_COLLECTION},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TEXT},
+     * @return list of matching cases, or an empty list when the field value is {@code null}
+     * @see ActionDelegate#findCase(DataField)
+     * @see ActionDelegate#findCase(String)
+     * @see ActionDelegate#findCase(Closure)
+     */
+    Case findCase(Field caseRef) {
+        if(caseRef.value == null) {
+            log.error("Value of field with id [${caseRef.importId}] is null, returning null.")
+            return null
+        }
+        try {
+            List<String> castValue = [caseRef.value].flatten() as List<String>
+            if(castValue.size() == 0) {
+                log.error("Value of field with id [${caseRef.importId}] does not contain at least one element, returning null.")
+                return null
+            }
+            return this.findCase(castValue[0])
+        } catch (ClassCastException e) {
+            log.error("Method cannot be used with field with id [${caseRef.importId}].", e)
+            return null
+        }
+    }
+
+
+    /**
+     * Finds the first case referenced by a dataField in its value.
+     *
+     * Use this overload when working on a case from out of current action context. For working with fields from the current
+     * action context see other overloads of this action.
+     *
+     * <p>If the field value is {@code null}, this method returns an empty list.</p>
+     * <p>If the value cannot be converted to case IDs, this method returns {@code null}.</p>
+     *
+     * @param caseRef field whose value contains case IDs, may be of types
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#CASE_REF},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#ENUMERATION_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#STRING_COLLECTION},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TEXT},
+     * @return list of matching cases, or an empty list when the field value is {@code null}
+     * @see ActionDelegate#findCase(Field)
+     * @see ActionDelegate#findCase(String)
+     * @see ActionDelegate#findCase(Closure)
+     */
+    Case findCase(DataField caseRef) {
+        if(caseRef.value == null) {
+            log.error("Value of field is null, returning null.")
+            return null
+        }
+        try {
+            List<String> castValue = [caseRef.value].flatten() as List<String>
+            if(castValue.size() == 0) {
+                log.error("Value of field does not contain at least one element, returning null.")
+                return null
+            }
+            return this.findCase(castValue[0])
+        } catch (ClassCastException e) {
+            log.error("Method cannot be used with field.", e)
+            return null
+        }
+    }
+
+    /**
+     * Finds case by its MongoDB ID.
+     *
+     * @param mongoId case IDs
+     * @return resulting case
+     * @see ActionDelegate#findCase(Field)
+     * @see ActionDelegate#findCase(DataField)
+     * @see ActionDelegate#findCase(Closure)
+     */
+    Case findCase(String mongoId) {
+        return workflowService.findOne(mongoId)
+    }
+
     Case createCase(String identifier, String title = null, String color = "", IUser author = userService.loggedOrSystem, Locale locale = LocaleContextHolder.getLocale(), Map<String, String> params = [:]) {
         return workflowService.createCaseByIdentifier(identifier, title, color, author.transformToLoggedUser(), locale, params).getCase()
     }
@@ -930,19 +1116,70 @@ class ActionDelegate {
         return outcome.getCase()
     }
 
+    /**
+     * Deletes a case by its MongoDB ID.
+     *
+     * @param mongoId case identifier
+     * @return deleted case, or {@code null} when the input is {@code null}
+     */
+    Case deleteCase(String mongoId) {
+        if(mongoId == null){
+            log.warn("Null value detected, returning null.")
+            return null
+        }
+        return this.deleteCase(workflowService.findOne(mongoId))
+    }
+
+    /**
+     * Deletes the provided case.
+     *
+     * @param toDelete case to delete
+     * @return deleted case, or {@code null} when the input is {@code null}
+     */
+    Case deleteCase(Case toDelete) {
+        if(toDelete == null){
+            log.warn("Null value detected, returning null.")
+            return null
+        }
+        return workflowService.deleteCase(toDelete).case
+    }
+
     Task assignTask(String transitionId, Case aCase = useCase, IUser user = userService.loggedOrSystem, Map<String, String> params = [:]) {
         String taskId = getTaskId(transitionId, aCase)
-        AssignTaskEventOutcome outcome = taskService.assignTask(user.transformToLoggedUser(), taskId, params)
-        this.outcomes.add(outcome)
-        return outcome.getTask()
+        return addTaskOutcomeAndReturnTask(taskService.assignTask(user.transformToLoggedUser(), taskId, params))
     }
 
     Task assignTask(Task task, IUser user = userService.loggedOrSystem, Map<String, String> params = [:]) {
         return addTaskOutcomeAndReturnTask(taskService.assignTask(task, user, params))
     }
 
-    void assignTasks(List<Task> tasks, IUser assignee = userService.loggedOrSystem, Map<String, String> params = [:]) {
-        this.outcomes.addAll(taskService.assignTasks(tasks, assignee, params))
+    /**
+     * Assigns tasks for all transitions in the provided list and returns the assigned tasks.
+     *
+     * @param transitionIds transition identifiers whose tasks should be assigned
+     * @param aCase case used to resolve the tasks, defaults to the current case
+     * @param user user to assign the tasks to, defaults to the logged or system user
+     * @param params additional parameters
+     * @return assigned tasks
+     */
+    List<Task> assignTasksByTransitions(List<String> transitionIds, Case aCase = useCase, IUser user = userService.loggedOrSystem, Map<String, String> params = [:]) {
+        List<String> taskIds = getTaskIds(transitionIds, aCase)
+        List<Task> tasks = taskService.findAllById(taskIds)
+        return assignTasks(tasks, user, params)
+    }
+
+    /**
+     * Assigns the provided tasks and returns the assigned tasks.
+     *
+     * @param tasks tasks to assign
+     * @param assignee user to assign the tasks to, defaults to the logged or system user
+     * @param params additional parameters
+     * @return assigned tasks
+     */
+    List<Task> assignTasks(List<Task> tasks, IUser assignee = userService.loggedOrSystem, Map<String, String> params = [:]) {
+        List<AssignTaskEventOutcome> outcomes = taskService.assignTasks(tasks, assignee, params)
+        this.outcomes.addAll(outcomes)
+        return outcomes.collect { it.task }
     }
 
     Task cancelTask(String transitionId, Case aCase = useCase, IUser user = userService.loggedOrSystem, Map<String, String> params = [:]) {
@@ -954,8 +1191,34 @@ class ActionDelegate {
         return addTaskOutcomeAndReturnTask(taskService.cancelTask(task, user, params))
     }
 
-    void cancelTasks(List<Task> tasks, IUser user = userService.loggedOrSystem, Map<String, String> params = [:]) {
-        this.outcomes.addAll(taskService.cancelTasks(tasks, user, params))
+
+    /**
+     * Cancels tasks for all transitions in the provided list and returns the canceled tasks.
+     *
+     * @param transitionIds transition identifiers whose tasks should be canceled
+     * @param aCase case used to resolve the tasks, defaults to the current case
+     * @param user user performing the cancellation, defaults to the logged or system user
+     * @param params additional parameters
+     * @return canceled tasks
+     */
+    List<Task> cancelTasksByTransitions(List<String> transitionIds, Case aCase = useCase, IUser user = userService.loggedOrSystem, Map<String, String> params = [:]) {
+        List<String> taskIds = getTaskIds(transitionIds, aCase)
+        List<Task> tasks = taskService.findAllById(taskIds)
+        return cancelTasks(tasks, user, params)
+    }
+
+    /**
+     * Cancels the provided tasks and returns the canceled tasks.
+     *
+     * @param tasks tasks to cancel
+     * @param user user performing the cancellation, defaults to the logged or system user
+     * @param params additional parameters
+     * @return canceled tasks
+     */
+    List<Task> cancelTasks(List<Task> tasks, IUser user = userService.loggedOrSystem, Map<String, String> params = [:]) {
+        List<CancelTaskEventOutcome> outcomes = taskService.cancelTasks(tasks, user, params)
+        this.outcomes.addAll(outcomes)
+        return outcomes.collect { it.task }
     }
 
     private Task addTaskOutcomeAndReturnTask(TaskEventOutcome outcome) {
@@ -963,17 +1226,42 @@ class ActionDelegate {
         return outcome.getTask()
     }
 
-    void finishTask(String transitionId, Case aCase = useCase, IUser user = userService.loggedOrSystem, Map<String, String> params = [:]) {
+    Task finishTask(String transitionId, Case aCase = useCase, IUser user = userService.loggedOrSystem, Map<String, String> params = [:]) {
         String taskId = getTaskId(transitionId, aCase)
-        addTaskOutcomeAndReturnTask(taskService.finishTask(user.transformToLoggedUser(), taskId, params))
+        return addTaskOutcomeAndReturnTask(taskService.finishTask(user.transformToLoggedUser(), taskId, params))
+    }
+
+    Task finishTask(Task task, IUser user = userService.loggedOrSystem, Map<String, String> params = [:]) {
+        return addTaskOutcomeAndReturnTask(taskService.finishTask(task, user, params))
     }
 
-    void finishTask(Task task, IUser user = userService.loggedOrSystem, Map<String, String> params = [:]) {
-        addTaskOutcomeAndReturnTask(taskService.finishTask(task, user, params))
+    /**
+     * Finishes tasks for all transitions in the provided list and returns the finished tasks.
+     *
+     * @param transitionIds transition identifiers whose tasks should be finished
+     * @param aCase case used to resolve the tasks, defaults to the current case
+     * @param user user performing the finish operation, defaults to the logged or system user
+     * @param params additional parameters
+     * @return finished tasks
+     */
+    List<Task> finishTasksByTransitions(List<String> transitionIds, Case aCase = useCase, IUser user = userService.loggedOrSystem, Map<String, String> params = [:]) {
+        List<String> taskIds = getTaskIds(transitionIds, aCase)
+        List<Task> tasks = taskService.findAllById(taskIds)
+        return finishTasks(tasks, user, params)
     }
 
-    void finishTasks(List<Task> tasks, IUser finisher = userService.loggedOrSystem, Map<String, String> params = [:]) {
-        this.outcomes.addAll(taskService.finishTasks(tasks, finisher, params))
+    /**
+     * Finishes the provided tasks and returns the finished tasks.
+     *
+     * @param tasks tasks to finish
+     * @param finisher user performing the finish operation, defaults to the logged or system user
+     * @param params additional parameters
+     * @return finished tasks
+     */
+    List<Task> finishTasks(List<Task> tasks, IUser finisher = userService.loggedOrSystem, Map<String, String> params = [:]) {
+        List<FinishTaskEventOutcome> outcomes = taskService.finishTasks(tasks, finisher)
+        this.outcomes.addAll(outcomes)
+        return outcomes.collect { it.task }
     }
 
     List<Task> findTasks(Closure<Predicate> predicate) {
@@ -988,13 +1276,262 @@ class ActionDelegate {
         return result.content
     }
 
+    /**
+     * Finds tasks referenced by a field in its value.
+     *
+     * Use this overload when working on a case from current action context. For working with fields from out of the
+     * current action context see other overloads of this action.
+     *
+     * <p>If the field value is {@code null}, this method returns an empty list.</p>
+     * <p>If the value cannot be converted to task IDs, this method returns {@code null}.</p>
+     *
+     * @param taskRef field whose value contains task IDs
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TASK_REF},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#ENUMERATION_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#STRING_COLLECTION},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TEXT},
+     * @return list of matching tasks, or an empty list when the field value is {@code null}
+     * @see ActionDelegate#findTasks(DataField)
+     * @see ActionDelegate#findTasks(List<String>)
+     */
+    List<Task> findTasks(Field taskRef) {
+        if(taskRef.value == null) {
+            log.error("Value of field with id [${taskRef.importId}] is null, returning empty list.")
+            return []
+        }
+        try {
+            return this.findTasks([taskRef.value].flatten() as List<String>)
+        } catch (ClassCastException e) {
+            log.error("Method cannot be used with field with id [${taskRef.importId}].", e)
+            return null
+        }
+    }
+    
+    /**
+     * Finds tasks referenced by a dataField in its value.
+     *
+     * Use this overload when working on a case not from the current action context. For working with fields from out of the
+     * current action context see other overloads of this action.
+     *
+     * <p>If the field value is {@code null}, this method returns an empty list.</p>
+     * <p>If the value cannot be converted to task IDs, this method returns {@code null}.</p>
+     *
+     * @param taskRef field whose value contains task IDs
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TASK_REF},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#ENUMERATION_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#STRING_COLLECTION},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TEXT},
+     * @return list of matching tasks, or an empty list when the field value is {@code null}
+     * @see ActionDelegate#findTasks(Field)
+     * @see ActionDelegate#findTasks(List<String>)
+     */
+    List<Task> findTasks(DataField taskRef) {
+        if(taskRef.value == null) {
+            log.error("Value of field is null, returning empty list.")
+            return []
+        }
+        try {
+            return this.findTasks([taskRef.value].flatten() as List<String>)
+        } catch (ClassCastException e) {
+            log.error("Method cannot be used with field.", e)
+            return null
+        }
+    }
+
+    /**
+     * Finds tasks by their MongoDB IDs.
+     *
+     * @param mongoIds task identifiers
+     * @return list of matching tasks, or an empty list when the input is {@code null}
+     * @see ActionDelegate#findTasks(Field)
+     * @see ActionDelegate#findTasks(DataField)
+     */
+    List<Task> findTasks(List<String> mongoIds) {
+        if(mongoIds == null) {
+            log.warn("Null value detected, returning empty list.")
+            return []
+        }
+        return taskService.findAllById(mongoIds)
+    }
+
     Task findTask(Closure<Predicate> predicate) {
         QTask qTask = new QTask("task")
         return taskService.searchOne(predicate(qTask))
     }
 
     Task findTask(String mongoId) {
-        return taskService.searchOne(QTask.task._id.eq(new ObjectId(mongoId)))
+        return taskService.findOne(mongoId)
+    }
+
+    /**
+     * Finds the first task referenced by a field in its value.
+     *
+     * Use this overload when working on a case from the current action context. For working with fields from out of the
+     * current action context see other overloads of this action.
+     *
+     * <p>If the field value is {@code null}, this method returns {@code null}.</p>
+     * <p>If the field contains no value or the value cannot be converted to a task ID, this method returns
+     * {@code null}.</p>
+     *
+     * @param taskRef field whose value contains a task ID
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TASK_REF},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#ENUMERATION_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#STRING_COLLECTION},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TEXT},
+     * @return referenced task, or {@code null} when the field value is invalid
+     * @see ActionDelegate#findTask(DataField)
+     * @see ActionDelegate#findTask(String)
+     * @see ActionDelegate#findTask(Closure)
+     */
+    Task findTask(Field taskRef) {
+        if(taskRef.value == null) {
+            log.error("Value of field with id [${taskRef.importId}] is null, returning null")
+            return null
+        }
+        try {
+            List<String> castValue = [taskRef.value].flatten() as List<String>
+            if(castValue.size() == 0) {
+                log.error("Value of field with id [${taskRef.importId}] does not contain at least one element, returning null.")
+                return null
+            }
+            return this.findTask(castValue[0])
+        } catch (ClassCastException e) {
+            log.error("Method cannot be used with field with id [${taskRef.importId}].", e)
+            return null
+        }
+    }
+
+    /**
+     * Finds the first task referenced by a dataField in its value.
+     *
+     * Use this overload when working on a case not from the current action context. For working with fields from out of the
+     * current action context see other overloads of this action.
+     *
+     * <p>If the field value is {@code null}, this method returns {@code null}.</p>
+     * <p>If the field contains no value or the value cannot be converted to a task ID, this method returns
+     * {@code null}.</p>
+     *
+     * @param taskRef field whose value contains a task ID
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TASK_REF},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#MULTICHOICE_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#ENUMERATION_MAP},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#STRING_COLLECTION},
+     * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TEXT},
+     * @return referenced task, or {@code null} when the field value is invalid
+     * @see ActionDelegate#findTask(Field)
+     * @see ActionDelegate#findTask(String)
+     * @see ActionDelegate#findTask(Closure)
+     */
+    Task findTask(DataField taskRef) {
+        if(taskRef.value == null) {
+            log.error("Value of field is null, returning null")
+            return null
+        }
+        try {
+            List<String> castValue = [taskRef.value].flatten() as List<String>
+            if(castValue.size() == 0) {
+                log.error("Value of field does not contain at least one element, returning null.")
+                return null
+            }
+            return taskService.findOne(castValue[0])
+        } catch (ClassCastException e) {
+            log.error("Method cannot be used with field.", e)
+            return null
+        }
+    }
+
+    /**
+     * Finds a Petri net by its MongoDB ID.
+     *
+     * @param mongoId Petri net identifier
+     * @return matching Petri net, or {@code null} when the input is {@code null}
+     */
+    PetriNet findPetriNet(String mongoId) {
+        if(mongoId == null){
+            log.warn("Null value detected, returning null.")
+            return null
+        }
+        return petriNetService.getPetriNet(mongoId)
+    }
+
+    /**
+     * Finds a Petri net by its {@link ObjectId}.
+     *
+     * @param objectId Petri net object identifier
+     * @return matching Petri net, or {@code null} when the input is {@code null}
+     */
+    PetriNet findPetriNet(ObjectId objectId) {
+        if(objectId == null){
+            log.warn("Null value detected, returning null.")
+            return null
+        }
+        return petriNetService.get(objectId)
+    }
+
+    /**
+     * Finds Petri nets by their MongoDB IDs.
+     *
+     * @param mongoIds list of Petri net identifiers
+     * @return matching Petri nets, or an empty list when the input is {@code null}
+     */
+    List<PetriNet> findPetriNets(List<String> mongoIds) {
+        if(mongoIds == null){
+            log.warn("Null value detected, returning empty list.")
+            return []
+        }
+        return petriNetService.findAllById(mongoIds)
+    }
+
+    /**
+     * Finds Petri nets by their {@link ObjectId} values.
+     *
+     * @param objectIds list of Petri net object identifiers
+     * @return matching Petri nets, or an empty list when the input is {@code null}
+     */
+    List<PetriNet> findPetriNetsByObjectIds(List<ObjectId> objectIds) {
+        if(objectIds == null){
+            log.warn("Null value detected, returning empty list.")
+            return []
+        }
+        return petriNetService.get(objectIds as Collection<ObjectId>)
+    }
+
+    /**
+     * Finds a Petri net by its identifier and optional version.
+     *
+     * If the version is not provided, the newest available version is returned.
+     *
+     * @param identifier Petri net identifier
+     * @param version requested version, or {@code null} for the newest version
+     * @return matching Petri net, or {@code null} when the identifier is {@code null}
+     */
+    PetriNet findPetriNetByIdentifier(String identifier, Version version = null) {
+        if(identifier == null) {
+            log.warn("Null identifier value detected, returning null.")
+            return null
+        }
+        return version == null ? petriNetService.getNewestVersionByIdentifier(identifier) : petriNetService.getPetriNet(identifier, version)
+    }
+
+    /**
+     * Converts cases to a map of option keys and translated option values.
+     *
+     * @param casesToTransform cases to convert
+     * @param valueTransformation transformation used to derive the option label from a case, case title is used if not specified otherwise
+     * @param keyTransformation transformation used to derive the option key from a case, case stringId is used if not specified otherwise
+     * @return map of option keys and translated values
+     */
+    Map<String, I18nString> casesToOptions(List<Case> casesToTransform, Closure<String> valueTransformation = { return it.title }, Closure<String> keyTransformation = { return it.stringId }) {
+        return casesToTransform.collectEntries {
+            [(keyTransformation(it)): new I18nString(valueTransformation(it))]
+        }
     }
 
     String getTaskId(String transitionId, Case aCase = useCase) {
@@ -1002,6 +1539,18 @@ class ActionDelegate {
         refs.find { it.transitionId == transitionId }.stringId
     }
 
+    /**
+     * Returns task identifiers for tasks belonging to the provided transitions in the given case.
+     *
+     * @param transitionIds transition identifiers
+     * @param aCase case whose tasks should be inspected, defaults to the current case
+     * @return list of matching task identifiers
+     */
+    List<String> getTaskIds(List<String> transitionIds, Case aCase = useCase) {
+        List<TaskReference> refs = taskService.findAllByCase(aCase.stringId, null)
+        return refs.findAll { transitionIds.contains(it.transitionId) }.collect { it.stringId}
+    }
+
     IUser assignRole(String roleMongoId, IUser user = userService.loggedUser) {
         IUser actualUser = userService.addRole(user, roleMongoId)
         return actualUser
@@ -1526,8 +2075,8 @@ class ActionDelegate {
     }
 
     /**
-     * Action API case search function using Elasticsearch database
-     * @param requests the CaseSearchRequest list
+     * Action API task search function using Elasticsearch database
+     * @param requests the @link{ElasticTaskSearchRequest} list
      * @param loggedUser the user who is searching for the requests
      * @param page the order of page to return. by default it returns the first page
      * @param pageable the page configuration that will contain the requests
@@ -1535,13 +2084,13 @@ class ActionDelegate {
      * @param isIntersection to decide null query handling
      * @return page of cases
      * */
-    Page<Task> findTasks(List<ElasticTaskSearchRequest> requests, LoggedUser loggedUser = userService.loggedOrSystem.transformToLoggedUser(),
+    Page<Task> findTasksElastic(List<ElasticTaskSearchRequest> requests, LoggedUser loggedUser = userService.loggedOrSystem.transformToLoggedUser(),
                          int page = 1, int pageSize = 25, Locale locale = Locale.default, boolean isIntersection = false) {
         return elasticTaskService.search(requests, loggedUser, PageRequest.of(page, pageSize), locale, isIntersection)
     }
 
     /**
-     * Action API case search function using Elasticsearch database
+     * Action API task search function using Elasticsearch database
      * @param request case search request
      * @param loggedUser the user who is searching for the requests
      * @param page the order of page to return. by default it returns the first page
@@ -1550,10 +2099,10 @@ class ActionDelegate {
      * @param isIntersection to decide null query handling
      * @return page of cases
      * */
-    Page<Task> findTasks(Map<String, Object> request, LoggedUser loggedUser = userService.loggedOrSystem.transformToLoggedUser(),
+    Page<Task> findTasksElastic(Map<String, Object> request, LoggedUser loggedUser = userService.loggedOrSystem.transformToLoggedUser(),
                          int page = 1, int pageSize = 25, Locale locale = Locale.default, boolean isIntersection = false) {
         List<ElasticTaskSearchRequest> requests = Collections.singletonList(new ElasticTaskSearchRequest(request))
-        return findTasks(requests, loggedUser, page, pageSize, locale, isIntersection)
+        return findTasksElastic(requests, loggedUser, page, pageSize, locale, isIntersection)
     }
 
     List<Case> findDefaultFilters() {
diff --git a/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy b/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy
index 4900ac8595b..1f7240f5a53 100644
--- a/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy
+++ b/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy
@@ -7,9 +7,20 @@ import com.netgrif.application.engine.auth.domain.IUser
 import com.netgrif.application.engine.auth.service.interfaces.IUserService
 import com.netgrif.application.engine.auth.web.requestbodies.NewUserRequest
 import com.netgrif.application.engine.configuration.PublicViewProperties
+import com.netgrif.application.engine.petrinet.domain.I18nString
+import com.netgrif.application.engine.petrinet.domain.PetriNet
+import com.netgrif.application.engine.petrinet.domain.VersionType
+import com.netgrif.application.engine.petrinet.domain.dataset.EnumerationMapField
+import com.netgrif.application.engine.petrinet.domain.dataset.Field
 import com.netgrif.application.engine.petrinet.domain.dataset.logic.action.ActionDelegate
+import com.netgrif.application.engine.petrinet.domain.version.Version
+import com.netgrif.application.engine.petrinet.service.interfaces.IPetriNetService
+import com.netgrif.application.engine.workflow.domain.Case
+import com.netgrif.application.engine.workflow.domain.DataField
+import com.netgrif.application.engine.workflow.domain.Task
 import com.netgrif.application.engine.workflow.service.interfaces.IFilterImportExportService
 import com.netgrif.application.engine.workflow.web.responsebodies.MessageResource
+import org.bson.types.ObjectId
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Disabled
 import org.junit.jupiter.api.Test
@@ -22,6 +33,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension
 import javax.mail.internet.MimeMessage
 
 import static java.util.Base64.*
+import static org.junit.jupiter.api.Assertions.assertThrows
+import static org.junit.jupiter.api.Assertions.assertTrue;
 
 @SpringBootTest
 @ActiveProfiles(["test"])
@@ -43,21 +56,27 @@ class ActionDelegateTest {
     @Autowired
     private PublicViewProperties publicViewProperties
 
+    @Autowired
+    private IPetriNetService petriNetService
+
+    private static final ACTION_API_NET_IDENTIFIER = "action_api_improvements"
+
     @BeforeEach
     void before() {
         testHelper.truncateDbs()
+        actionDelegate.outcomes = []
     }
 
     @Test
     @Disabled("Context user")
-    void importFiltersTest(){
+    void importFiltersTest() {
         List<String> actionDelegateList = actionDelegate.importFilters()
         List<String> importedTasksIds = importExportService.importFilters()
         assert actionDelegateList.size() == importedTasksIds.size()
     }
 
     @Test
-    void inviteUser(){
+    void inviteUser() {
         GreenMail smtpServer = new GreenMail(new ServerSetup(2525, null, "smtp"))
         smtpServer.start()
 
@@ -70,7 +89,7 @@ class ActionDelegateTest {
     }
 
     @Test
-    void deleteUser(){
+    void deleteUser() {
         GreenMail smtpServer = new GreenMail(new ServerSetup(2525, null, "smtp"))
         smtpServer.start()
         String mail = "test@netgrif.com";
@@ -88,7 +107,7 @@ class ActionDelegateTest {
 
 
     @Test
-    void inviteUserNewUserRequest(){
+    void inviteUserNewUserRequest() {
         GreenMail smtpServer = new GreenMail(new ServerSetup(2525, null, "smtp"))
         smtpServer.start()
 
@@ -113,4 +132,328 @@ class ActionDelegateTest {
         assert actionDelegate.makeUrl(publicViewProperties.url, identifier) == url
         assert actionDelegate.makeUrl("test.netgrif.com/public", "identifier") == "test.netgrif.com/public/${getEncoder().encodeToString(identifier.bytes)}"
     }
+
+    @Test
+    void testTaskActions() {
+        importTestPetriNet()
+        Case testCase = actionDelegate.createCase(ACTION_API_NET_IDENTIFIER)
+        List<String> taskIds = testCase.tasks.collect { it.task }
+        List<Task> tasks = actionDelegate.findTasks(taskIds)
+
+        assert tasks != null
+        assert !tasks.empty
+        assert tasks.size() == taskIds.size()
+
+        testCase = actionDelegate.setData("t1", testCase, [
+                "enumeration_map" : [
+                        "type" : "enumeration_map",
+                        "value": taskIds[0]
+                ],
+                "multichoice_map" : [
+                        "type" : "multichoice_map",
+                        "value": taskIds
+                ],
+                "stringCollection": [
+                        "type" : "stringCollection",
+                        "value": taskIds
+                ],
+                "taskRef"         : [
+                        "type" : "taskRef",
+                        "value": taskIds
+                ],
+                "text"            : [
+                        "type" : "text",
+                        "value": taskIds[0]
+                ]
+        ]).case
+        actionDelegate.useCase = testCase
+
+//        fields with single value
+        ["enumeration_map", "text"].forEach {
+            assertTaskSearchResults(it, testCase, 1, true)
+        }
+
+//        fields with collection value
+        ["multichoice_map", "stringCollection", "taskRef"].forEach {
+            assertTaskSearchResults(it, testCase, taskIds.size(), false)
+        }
+    }
+
+    @Test
+    void testCaseActions() {
+        importTestPetriNet()
+        Case testCase1 = actionDelegate.createCase(ACTION_API_NET_IDENTIFIER)
+        Case testCase2 = actionDelegate.createCase(ACTION_API_NET_IDENTIFIER)
+        Case testCase3 = actionDelegate.createCase(ACTION_API_NET_IDENTIFIER)
+        Case testCase4 = actionDelegate.createCase(ACTION_API_NET_IDENTIFIER)
+
+        List<String> caseIds = [testCase1.stringId, testCase2.stringId, testCase3.stringId, testCase4.stringId]
+
+        Case searchedCase = actionDelegate.findCase(caseIds[0])
+        assert searchedCase != null
+        assert searchedCase.stringId == testCase1.stringId
+
+        List<Case> searchedCases = actionDelegate.findCases(caseIds)
+        assert searchedCases != null
+        assert !searchedCases.empty
+        assert searchedCases.size() == caseIds.size()
+
+
+        testCase1 = actionDelegate.setData("t1", testCase1, [
+                "enumeration_map" : [
+                        "type" : "enumeration_map",
+                        "value": caseIds[0]
+                ],
+                "multichoice_map" : [
+                        "type" : "multichoice_map",
+                        "value": caseIds
+                ],
+                "stringCollection": [
+                        "type" : "stringCollection",
+                        "value": caseIds
+                ],
+                "caseRef"         : [
+                        "type" : "caseRef",
+                        "value": caseIds
+                ],
+                "text"            : [
+                        "type" : "text",
+                        "value": caseIds[0]
+                ]
+        ]).case
+
+        actionDelegate.useCase = testCase1
+//        fields with single value
+        ["enumeration_map", "text"].forEach {
+            assertCaseSearchResults(it, testCase1, 1, true)
+        }
+
+//        fields with collection value
+        ["multichoice_map", "stringCollection", "caseRef"].forEach {
+            assertCaseSearchResults(it, testCase1, caseIds.size(), false)
+        }
+    }
+
+    @Test
+    void testCaseDeletionActions() {
+        importTestPetriNet()
+        Case testCase1 = actionDelegate.createCase(ACTION_API_NET_IDENTIFIER)
+        Case testCase2 = actionDelegate.createCase(ACTION_API_NET_IDENTIFIER)
+
+        testCase1 = actionDelegate.deleteCase(testCase1)
+        assertCaseDeletion(testCase1.stringId)
+
+        testCase2 = actionDelegate.deleteCase(testCase2.stringId)
+        assertCaseDeletion(testCase2.stringId)
+    }
+
+    @Test
+    void testPetriNetActions() {
+        PetriNet importedNet = importTestPetriNet()
+        String mongoId = importedNet.stringId
+        String netIdentifier = importedNet.getIdentifier()
+        ObjectId objectId = importedNet.objectId
+
+        PetriNet foundNet = actionDelegate.findPetriNet(mongoId)
+        assert foundNet != null
+        assert foundNet.objectId == objectId
+        assert foundNet.identifier == netIdentifier
+        foundNet = null
+
+        foundNet = actionDelegate.findPetriNet(objectId)
+        assert foundNet != null
+        assert foundNet.stringId == mongoId
+        assert foundNet.identifier == netIdentifier
+        foundNet = null
+
+        PetriNet filterNet = petriNetService.getByIdentifier("filter")[0]
+        String mongoId2 = filterNet.stringId
+        String netIdentifier2 = filterNet.getIdentifier()
+        ObjectId objectId3 = filterNet.objectId
+
+        def searchTargets = [mongoId, mongoId2]
+        List<PetriNet> searchResults = actionDelegate.findPetriNets(searchTargets as List<String>)
+        assert searchResults != null
+        assert !searchResults.empty
+        assert searchResults.collect { it.stringId }.containsAll(searchTargets)
+        searchResults = null
+
+        searchTargets = [objectId, objectId]
+        searchResults = actionDelegate.findPetriNetsByObjectIds(searchTargets as List<ObjectId>)
+        assert searchResults != null
+        assert !searchResults.empty
+        assert searchResults.collect { it.objectId }.containsAll(searchTargets)
+
+        PetriNet importedNet2 = importTestPetriNet()
+        assert importedNet2 != null
+        assert importedNet2.identifier == netIdentifier
+        assert importedNet2.stringId != mongoId
+        assert importedNet2.version != importedNet.version
+
+        foundNet = actionDelegate.findPetriNetByIdentifier(importedNet2.identifier, new Version(1, 0, 0))
+        assert foundNet != null
+        assert foundNet.identifier == netIdentifier
+        assert foundNet.stringId == mongoId
+        assert foundNet.version == importedNet.version
+        assert foundNet.version != importedNet2.version
+        foundNet = null
+
+//        find newest
+        foundNet = actionDelegate.findPetriNetByIdentifier(importedNet2.identifier)
+        assert foundNet != null
+        assert foundNet.identifier == netIdentifier
+        assert foundNet.stringId != mongoId
+        assert foundNet.version != importedNet.version
+        assert foundNet.version == importedNet2.version
+    }
+
+    @Test
+    void testOptionsActions() {
+        PetriNet net = importTestPetriNet()
+        Case case1 = actionDelegate.createCase(net, "Test title 1")
+        Case case2 = actionDelegate.createCase(net, "Test title 2")
+
+        List<Case> cases = [case1, case2]
+        Map<String, I18nString> options = actionDelegate.casesToOptions(cases)
+        assert options != null
+        assert options.size() == cases.size()
+        assert options.keySet().containsAll(cases.collect { it.stringId })
+        assert options.get(case1.stringId).defaultValue == case1.title
+        assert options.get(case2.stringId).defaultValue == case2.title
+        options = null
+
+        String keyTransformationTestString = "Key transformation test "
+        String valueTransformationTestString = "Value transformation test "
+        options = actionDelegate.casesToOptions(cases, { return "Value transformation test ".concat(it.title) }, { return "Key transformation test ".concat(it.stringId) })
+        assert options != null
+        assert options.size() == cases.size()
+        assert options.keySet().containsAll(cases.collect { keyTransformationTestString.concat(it.stringId) })
+        assert options.get(keyTransformationTestString.concat(case1.stringId)).defaultValue == valueTransformationTestString.concat(case1.title)
+        assert options.get(keyTransformationTestString.concat(case2.stringId)).defaultValue == valueTransformationTestString.concat(case2.title)
+
+        case1.dataSet["enumeration_map"].options = options
+        actionDelegate.useCase = case1
+        actionDelegate.initFieldsMap(["enumeration_map": "enumeration_map"])
+        EnumerationMapField field = (EnumerationMapField) actionDelegate.map.get("enumeration_map")
+        assert field != null
+        assert field.options != null
+        I18nString option = field.getOption(keyTransformationTestString.concat(case1.stringId))
+        assert option != null
+        assert option.defaultValue == valueTransformationTestString.concat(case1.title)
+    }
+
+    @Test
+    void testTaskEventActions() {
+        importTestPetriNet()
+        Case testCase = actionDelegate.createCase(ACTION_API_NET_IDENTIFIER)
+        List<String> transitionIds = ["t1", "t2"]
+        IUser user = userService.getLoggedOrSystem()
+
+//        testing "byTransition" variants should be enough, as they call methods, that tak List<Task> instead of List<String>
+        List<Task> tasks = actionDelegate.assignTasksByTransitions(transitionIds, testCase)
+        assert tasks != null
+        assert tasks.size() == transitionIds.size()
+        assert tasks.stream().allMatch { it.user.email == user.email }
+        assert tasks.stream().allMatch { it.userId == user.stringId }
+        assert tasks.stream().allMatch { it.startDate != null }
+        tasks = null
+
+        tasks = actionDelegate.cancelTasksByTransitions(transitionIds, testCase)
+        assert tasks != null
+        assert tasks.size() == transitionIds.size()
+        assert tasks.stream().allMatch { it.userId == null }
+        assert tasks.stream().allMatch { it.startDate == null }
+        tasks = null
+
+        actionDelegate.assignTasksByTransitions(transitionIds, testCase)
+        tasks = actionDelegate.finishTasksByTransitions(transitionIds, testCase)
+        assert tasks != null
+        assert tasks.size() == transitionIds.size()
+        assert tasks.stream().allMatch { it.finishedBy == user.stringId }
+        assert tasks.stream().allMatch { it.finishDate != null }
+        assert tasks.stream().allMatch { it.userId == null }
+        tasks = actionDelegate.findTasks(tasks.collect { it.stringId })
+        assert tasks.size() == 1
+        assert tasks[0].transitionId == "t2"
+    }
+
+    private void assertCaseDeletion(String deletedCaseId) {
+        Exception e = assertThrows(IllegalArgumentException.class, () -> {
+            actionDelegate.findCase(deletedCaseId)
+        })
+
+        String expectedMessage = "Could not find Case with id [${deletedCaseId}]"
+        assertTrue(expectedMessage == e.getMessage())
+    }
+
+    private void assertTaskSearchResults(String fieldId, Case testCase, int sizeToCheck, boolean singleValueField) {
+        actionDelegate.initFieldsMap([(fieldId): fieldId])
+        Field field = actionDelegate.map.get(fieldId)
+        String firstTaskId = ([field.value].flatten() as List<String>)[0]
+
+        Task task = actionDelegate.findTask(field)
+        assert fieldId && task != null
+        assert fieldId && task.stringId == firstTaskId
+        task = null
+
+        List<Task> tasks = actionDelegate.findTasks(field)
+        assert fieldId && tasks != null
+        assert fieldId && !tasks.empty
+        assert fieldId && tasks.size() == sizeToCheck
+        if (singleValueField) {
+            assert fieldId && tasks[0].stringId == firstTaskId
+        }
+        tasks = null
+
+        DataField dataField = testCase.getDataField(fieldId)
+        task = actionDelegate.findTask(dataField)
+        assert fieldId && task != null
+        assert fieldId && task.stringId == firstTaskId
+
+        tasks = actionDelegate.findTasks(dataField)
+        assert fieldId && tasks != null
+        assert fieldId && !tasks.empty
+        assert fieldId && tasks.size() == sizeToCheck
+        if (singleValueField) {
+            assert fieldId && tasks[0].stringId == firstTaskId
+        }
+    }
+
+    private void assertCaseSearchResults(String fieldId, Case testCase, int sizeToCheck, boolean singleValueField) {
+        actionDelegate.initFieldsMap([(fieldId): fieldId])
+        Field field = actionDelegate.map.get(fieldId)
+        String firstCaseId = ([field.value].flatten() as List<String>)[0]
+
+        Case searchedCase = actionDelegate.findCase(field)
+        assert fieldId && searchedCase != null
+        assert fieldId && searchedCase.stringId == firstCaseId
+        searchedCase = null
+
+        List<Case> searchedCases = actionDelegate.findCases(field)
+        assert fieldId && searchedCases != null
+        assert fieldId && !searchedCases.empty
+        assert fieldId && searchedCases.size() == sizeToCheck
+        if (singleValueField) {
+            assert fieldId && searchedCases[0].stringId == firstCaseId
+        }
+        searchedCases = null
+
+
+        DataField dataField = testCase.getDataField(fieldId)
+        searchedCase = actionDelegate.findCase(dataField)
+        assert fieldId && searchedCase != null
+        assert fieldId && searchedCase.stringId == firstCaseId
+
+        searchedCases = actionDelegate.findCases(dataField)
+        assert fieldId && searchedCases != null
+        assert fieldId && !searchedCases.empty
+        assert fieldId && searchedCases.size() == sizeToCheck
+        if (singleValueField) {
+            assert fieldId && searchedCases[0].stringId == firstCaseId
+        }
+    }
+
+    private PetriNet importTestPetriNet() {
+        return petriNetService.importPetriNet(new FileInputStream("src/test/resources/petriNets/NAE-2390_action_api_improvements.xml"), VersionType.MAJOR, userService.getLoggedOrSystem().transformToLoggedUser()).getNet()
+    }
 }
diff --git a/src/test/resources/petriNets/NAE-2390_action_api_improvements.xml b/src/test/resources/petriNets/NAE-2390_action_api_improvements.xml
new file mode 100644
index 00000000000..6c201f9ea07
--- /dev/null
+++ b/src/test/resources/petriNets/NAE-2390_action_api_improvements.xml
@@ -0,0 +1,214 @@
+<document xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="https://petriflow.com/petriflow.schema.xsd">
+	<id>action_api_improvements</id>
+	<version>1.0.0</version>
+	<initials>NEW</initials>
+	<title>New Model</title>
+	<icon>device_hub</icon>
+	<defaultRole>true</defaultRole>
+	<anonymousRole>true</anonymousRole>
+	<transitionRole>false</transitionRole>
+	<data type="caseRef">
+		<id>caseRef</id>
+		<title/>
+		<allowedNets>
+			<allowedNet>action_api_improvements</allowedNet>
+		</allowedNets>
+	</data>
+	<data type="enumeration">
+		<id>enumeration</id>
+		<title/>
+	</data>
+	<data type="enumeration_map">
+		<id>enumeration_map</id>
+		<title/>
+	</data>
+	<data type="multichoice">
+		<id>multichoice</id>
+		<title/>
+	</data>
+	<data type="multichoice_map">
+		<id>multichoice_map</id>
+		<title/>
+	</data>
+	<data type="stringCollection">
+		<id>stringCollection</id>
+		<title/>
+	</data>
+	<data type="taskRef">
+		<id>taskRef</id>
+		<title/>
+	</data>
+	<data type="text">
+		<id>text</id>
+		<title/>
+	</data>
+	<transition>
+		<id>t1</id>
+		<x>336</x>
+		<y>112</y>
+		<label/>
+		<dataGroup>
+			<id>t1_0</id>
+			<cols>4</cols>
+			<layout>grid</layout>
+			<dataRef>
+				<id>enumeration_map</id>
+				<logic>
+					<behavior>editable</behavior>
+				</logic>
+				<layout>
+					<x>0</x>
+					<y>0</y>
+					<rows>1</rows>
+					<cols>2</cols>
+					<template>material</template>
+					<appearance>outline</appearance>
+				</layout>
+			</dataRef>
+			<dataRef>
+				<id>caseRef</id>
+				<logic>
+					<behavior>editable</behavior>
+				</logic>
+				<layout>
+					<x>2</x>
+					<y>0</y>
+					<rows>1</rows>
+					<cols>2</cols>
+					<template>material</template>
+					<appearance>outline</appearance>
+				</layout>
+			</dataRef>
+			<dataRef>
+				<id>taskRef</id>
+				<logic>
+					<behavior>editable</behavior>
+				</logic>
+				<layout>
+					<x>0</x>
+					<y>1</y>
+					<rows>1</rows>
+					<cols>4</cols>
+					<template>material</template>
+					<appearance>outline</appearance>
+				</layout>
+			</dataRef>
+			<dataRef>
+				<id>stringCollection</id>
+				<logic>
+					<behavior>editable</behavior>
+				</logic>
+				<layout>
+					<x>0</x>
+					<y>2</y>
+					<rows>1</rows>
+					<cols>2</cols>
+					<template>material</template>
+					<appearance>outline</appearance>
+				</layout>
+			</dataRef>
+			<dataRef>
+				<id>enumeration</id>
+				<logic>
+					<behavior>editable</behavior>
+				</logic>
+				<layout>
+					<x>2</x>
+					<y>2</y>
+					<rows>1</rows>
+					<cols>2</cols>
+					<template>material</template>
+					<appearance>outline</appearance>
+				</layout>
+			</dataRef>
+			<dataRef>
+				<id>multichoice</id>
+				<logic>
+					<behavior>editable</behavior>
+				</logic>
+				<layout>
+					<x>0</x>
+					<y>3</y>
+					<rows>1</rows>
+					<cols>2</cols>
+					<template>material</template>
+					<appearance>outline</appearance>
+				</layout>
+			</dataRef>
+			<dataRef>
+				<id>text</id>
+				<logic>
+					<behavior>editable</behavior>
+				</logic>
+				<layout>
+					<x>2</x>
+					<y>3</y>
+					<rows>1</rows>
+					<cols>2</cols>
+					<template>material</template>
+					<appearance>outline</appearance>
+				</layout>
+			</dataRef>
+			<dataRef>
+				<id>multichoice_map</id>
+				<logic>
+					<behavior>editable</behavior>
+				</logic>
+				<layout>
+					<x>2</x>
+					<y>4</y>
+					<rows>1</rows>
+					<cols>2</cols>
+					<template>material</template>
+					<appearance>outline</appearance>
+				</layout>
+			</dataRef>
+		</dataGroup>
+	</transition>
+	<transition>
+		<id>t2</id>
+		<x>208</x>
+		<y>240</y>
+		<label/>
+	</transition>
+	<transition>
+		<id>t3</id>
+		<x>336</x>
+		<y>240</y>
+		<label/>
+	</transition>
+	<transition>
+		<id>t4</id>
+		<x>464</x>
+		<y>240</y>
+		<label/>
+	</transition>
+	<place>
+		<id>p1</id>
+		<x>208</x>
+		<y>112</y>
+		<tokens>1</tokens>
+		<static>false</static>
+	</place>
+	<place>
+		<id>p2</id>
+		<x>464</x>
+		<y>112</y>
+		<tokens>0</tokens>
+		<static>false</static>
+	</place>
+	<arc>
+		<id>a1</id>
+		<type>regular</type>
+		<sourceId>p1</sourceId>
+		<destinationId>t1</destinationId>
+		<multiplicity>1</multiplicity>
+	</arc>
+	<arc>
+		<id>a2</id>
+		<type>regular</type>
+		<sourceId>t1</sourceId>
+		<destinationId>p2</destinationId>
+		<multiplicity>1</multiplicity>
+	</arc>
+</document>
\ No newline at end of file

From a1986acaea8be783ee191ce1aeac91cadab9d081 Mon Sep 17 00:00:00 2001
From: palajsamuel <palaj@netgrif.com>
Date: Thu, 23 Apr 2026 14:33:30 +0200
Subject: [PATCH 2/8] [NAE-2390] Action API Improvements

- implemented new action API method to ActionDelegate
- added getOption method to MapOptionsField
- tests for new functionality added to ActionDelegateTest
- test xml NAE-2390_action_api_improvements.xml added
---
 .../logic/action/ActionDelegate.groovy        | 30 ++++-----
 .../engine/action/ActionDelegateTest.groovy   | 67 +++++++++++--------
 .../NAE-2390_action_api_improvements.xml      |  2 +-
 3 files changed, 56 insertions(+), 43 deletions(-)

diff --git a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
index 69391895d78..6d401e02f80 100644
--- a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
+++ b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
@@ -984,7 +984,7 @@ class ActionDelegate {
             return this.findCases([caseRef.value].flatten() as List<String>)
         } catch (ClassCastException e) {
             log.error("Method cannot be used with field.", e)
-            return null
+            return []
         }
     }
 
@@ -993,14 +993,14 @@ class ActionDelegate {
      * Finds cases by their MongoDB IDs.
      *
      * @param mongoIds list of case IDs
-     * @return list of matching cases, or an empty list when the input is {@code null}
+     * @return list of matching cases, or an empty list when the input is {@code null} or {@code empty}
      * @see ActionDelegate#findCases(Field)
      * @see ActionDelegate#findCases(DataField)
      * @see ActionDelegate#findCases(Closure)
      * @see ActionDelegate#findCases(Closure, Pageable)
      */
     List<Case> findCases(List<String> mongoIds) {
-        if(mongoIds == null) {
+        if(mongoIds == null || mongoIds.empty) {
             log.warn("Null value detected, returning empty list.")
             return []
         }
@@ -1020,7 +1020,7 @@ class ActionDelegate {
      * Use this overload when working on a case from current action context. For working with fields from out of the
      * current action context see other overloads of this action.
      *
-     * <p>If the field value is {@code null}, this method returns an empty list.</p>
+     * <p>If the field value is {@code null}, this method returns {@code null}.</p>
      * <p>If the value cannot be converted to case IDs, this method returns {@code null}.</p>
      *
      * @param caseRef field whose value contains case IDs, may be of types
@@ -1030,7 +1030,7 @@ class ActionDelegate {
      * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#ENUMERATION_MAP},
      * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#STRING_COLLECTION},
      * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TEXT},
-     * @return list of matching cases, or an empty list when the field value is {@code null}
+     * @return referenced case, or {@code null} when the field value is invalid
      * @see ActionDelegate#findCase(DataField)
      * @see ActionDelegate#findCase(String)
      * @see ActionDelegate#findCase(Closure)
@@ -1060,7 +1060,7 @@ class ActionDelegate {
      * Use this overload when working on a case from out of current action context. For working with fields from the current
      * action context see other overloads of this action.
      *
-     * <p>If the field value is {@code null}, this method returns an empty list.</p>
+     * <p>If the field value is {@code null}, this method returns {@code null}.</p>
      * <p>If the value cannot be converted to case IDs, this method returns {@code null}.</p>
      *
      * @param caseRef field whose value contains case IDs, may be of types
@@ -1070,7 +1070,7 @@ class ActionDelegate {
      * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#ENUMERATION_MAP},
      * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#STRING_COLLECTION},
      * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TEXT},
-     * @return list of matching cases, or an empty list when the field value is {@code null}
+     * @return referenced case, or {@code null} when the dataField value is invalid
      * @see ActionDelegate#findCase(Field)
      * @see ActionDelegate#findCase(String)
      * @see ActionDelegate#findCase(Closure)
@@ -1259,7 +1259,7 @@ class ActionDelegate {
      * @return finished tasks
      */
     List<Task> finishTasks(List<Task> tasks, IUser finisher = userService.loggedOrSystem, Map<String, String> params = [:]) {
-        List<FinishTaskEventOutcome> outcomes = taskService.finishTasks(tasks, finisher)
+        List<FinishTaskEventOutcome> outcomes = taskService.finishTasks(tasks, finisher, params)
         this.outcomes.addAll(outcomes)
         return outcomes.collect { it.task }
     }
@@ -1346,12 +1346,12 @@ class ActionDelegate {
      * Finds tasks by their MongoDB IDs.
      *
      * @param mongoIds task identifiers
-     * @return list of matching tasks, or an empty list when the input is {@code null}
+     * @return list of matching tasks, or an empty list when the input is {@code null} org {@code empty}
      * @see ActionDelegate#findTasks(Field)
      * @see ActionDelegate#findTasks(DataField)
      */
     List<Task> findTasks(List<String> mongoIds) {
-        if(mongoIds == null) {
+        if(mongoIds == null || mongoIds.empty) {
             log.warn("Null value detected, returning empty list.")
             return []
         }
@@ -1440,7 +1440,7 @@ class ActionDelegate {
                 log.error("Value of field does not contain at least one element, returning null.")
                 return null
             }
-            return taskService.findOne(castValue[0])
+            return this.findTask(castValue[0])
         } catch (ClassCastException e) {
             log.error("Method cannot be used with field.", e)
             return null
@@ -1479,10 +1479,10 @@ class ActionDelegate {
      * Finds Petri nets by their MongoDB IDs.
      *
      * @param mongoIds list of Petri net identifiers
-     * @return matching Petri nets, or an empty list when the input is {@code null}
+     * @return matching Petri nets, or an empty list when the input is {@code null} or {@code empty}
      */
     List<PetriNet> findPetriNets(List<String> mongoIds) {
-        if(mongoIds == null){
+        if(mongoIds == null || mongoIds.empty){
             log.warn("Null value detected, returning empty list.")
             return []
         }
@@ -1493,10 +1493,10 @@ class ActionDelegate {
      * Finds Petri nets by their {@link ObjectId} values.
      *
      * @param objectIds list of Petri net object identifiers
-     * @return matching Petri nets, or an empty list when the input is {@code null}
+     * @return matching Petri nets, or an empty list when the input is {@code null} or {@code empty}
      */
     List<PetriNet> findPetriNetsByObjectIds(List<ObjectId> objectIds) {
-        if(objectIds == null){
+        if(objectIds == null || objectIds.empty){
             log.warn("Null value detected, returning empty list.")
             return []
         }
diff --git a/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy b/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy
index 1f7240f5a53..dc10532dbd3 100644
--- a/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy
+++ b/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy
@@ -34,7 +34,7 @@ import javax.mail.internet.MimeMessage
 
 import static java.util.Base64.*
 import static org.junit.jupiter.api.Assertions.assertThrows
-import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.junit.jupiter.api.Assertions.assertEquals;
 
 @SpringBootTest
 @ActiveProfiles(["test"])
@@ -60,6 +60,9 @@ class ActionDelegateTest {
     private IPetriNetService petriNetService
 
     private static final ACTION_API_NET_IDENTIFIER = "action_api_improvements"
+    private static final ERROR_MESSAGE_TEMPLATE = "field [|fieldId|] in [|testedMethod|] method returned null"
+    private static final FIELD_ID_TEMPLATE = "|fieldId|"
+    private static final TESTED_METHOD_TEMPLATE = "|testedMethod|"
 
     @BeforeEach
     void before() {
@@ -349,7 +352,7 @@ class ActionDelegateTest {
         List<String> transitionIds = ["t1", "t2"]
         IUser user = userService.getLoggedOrSystem()
 
-//        testing "byTransition" variants should be enough, as they call methods, that tak List<Task> instead of List<String>
+//        testing "byTransition" variants should be enough, as they call methods, that take List<Task> instead of List<String>
         List<Task> tasks = actionDelegate.assignTasksByTransitions(transitionIds, testCase)
         assert tasks != null
         assert tasks.size() == transitionIds.size()
@@ -383,73 +386,83 @@ class ActionDelegateTest {
         })
 
         String expectedMessage = "Could not find Case with id [${deletedCaseId}]"
-        assertTrue(expectedMessage == e.getMessage())
+        assertEquals(expectedMessage, e.getMessage())
     }
 
     private void assertTaskSearchResults(String fieldId, Case testCase, int sizeToCheck, boolean singleValueField) {
         actionDelegate.initFieldsMap([(fieldId): fieldId])
+        String errorMessageWithFieldId = ERROR_MESSAGE_TEMPLATE.replace(FIELD_ID_TEMPLATE, fieldId)
         Field field = actionDelegate.map.get(fieldId)
         String firstTaskId = ([field.value].flatten() as List<String>)[0]
 
         Task task = actionDelegate.findTask(field)
-        assert fieldId && task != null
-        assert fieldId && task.stringId == firstTaskId
+        String errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findTask(Field)")
+        assert task != null : errorMessage
+        assert task.stringId == firstTaskId : errorMessage
         task = null
 
         List<Task> tasks = actionDelegate.findTasks(field)
-        assert fieldId && tasks != null
-        assert fieldId && !tasks.empty
-        assert fieldId && tasks.size() == sizeToCheck
+        errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findTasks(Field)")
+        assert tasks != null : errorMessage
+        assert !tasks.empty : errorMessage
+        assert tasks.size() == sizeToCheck : errorMessage
         if (singleValueField) {
-            assert fieldId && tasks[0].stringId == firstTaskId
+            assert tasks[0].stringId == firstTaskId : errorMessage
         }
         tasks = null
 
         DataField dataField = testCase.getDataField(fieldId)
         task = actionDelegate.findTask(dataField)
-        assert fieldId && task != null
-        assert fieldId && task.stringId == firstTaskId
+        errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findTask(DataField)")
+        assert task != null : errorMessage
+        assert task.stringId == firstTaskId : errorMessage
 
         tasks = actionDelegate.findTasks(dataField)
-        assert fieldId && tasks != null
-        assert fieldId && !tasks.empty
-        assert fieldId && tasks.size() == sizeToCheck
+        errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findTasks(DataField)")
+        assert tasks != null : errorMessage
+        assert !tasks.empty : errorMessage
+        assert tasks.size() == sizeToCheck : errorMessage
         if (singleValueField) {
-            assert fieldId && tasks[0].stringId == firstTaskId
+            assert tasks[0].stringId == firstTaskId : errorMessage
         }
     }
 
     private void assertCaseSearchResults(String fieldId, Case testCase, int sizeToCheck, boolean singleValueField) {
         actionDelegate.initFieldsMap([(fieldId): fieldId])
+        String errorMessageWithFieldId = ERROR_MESSAGE_TEMPLATE.replace(FIELD_ID_TEMPLATE, fieldId)
         Field field = actionDelegate.map.get(fieldId)
         String firstCaseId = ([field.value].flatten() as List<String>)[0]
 
         Case searchedCase = actionDelegate.findCase(field)
-        assert fieldId && searchedCase != null
-        assert fieldId && searchedCase.stringId == firstCaseId
+        String errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findCase(Field)")
+        assert searchedCase != null : errorMessage
+        assert searchedCase.stringId == firstCaseId : errorMessage
         searchedCase = null
 
         List<Case> searchedCases = actionDelegate.findCases(field)
-        assert fieldId && searchedCases != null
-        assert fieldId && !searchedCases.empty
-        assert fieldId && searchedCases.size() == sizeToCheck
+        errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findCases(Field)")
+        assert searchedCases != null : errorMessage
+        assert !searchedCases.empty : errorMessage
+        assert searchedCases.size() == sizeToCheck : errorMessage
         if (singleValueField) {
-            assert fieldId && searchedCases[0].stringId == firstCaseId
+            assert searchedCases[0].stringId == firstCaseId : errorMessage
         }
         searchedCases = null
 
 
         DataField dataField = testCase.getDataField(fieldId)
         searchedCase = actionDelegate.findCase(dataField)
-        assert fieldId && searchedCase != null
-        assert fieldId && searchedCase.stringId == firstCaseId
+        errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findCase(DataField)")
+        assert searchedCase != null: errorMessage
+        assert searchedCase.stringId == firstCaseId: errorMessage
 
         searchedCases = actionDelegate.findCases(dataField)
-        assert fieldId && searchedCases != null
-        assert fieldId && !searchedCases.empty
-        assert fieldId && searchedCases.size() == sizeToCheck
+        errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findCases(DataField)")
+        assert searchedCases != null: errorMessage
+        assert !searchedCases.empty: errorMessage
+        assert searchedCases.size() == sizeToCheck: errorMessage
         if (singleValueField) {
-            assert fieldId && searchedCases[0].stringId == firstCaseId
+            assert searchedCases[0].stringId == firstCaseId: errorMessage
         }
     }
 
diff --git a/src/test/resources/petriNets/NAE-2390_action_api_improvements.xml b/src/test/resources/petriNets/NAE-2390_action_api_improvements.xml
index 6c201f9ea07..4cd61d922b0 100644
--- a/src/test/resources/petriNets/NAE-2390_action_api_improvements.xml
+++ b/src/test/resources/petriNets/NAE-2390_action_api_improvements.xml
@@ -2,7 +2,7 @@
 	<id>action_api_improvements</id>
 	<version>1.0.0</version>
 	<initials>NEW</initials>
-	<title>New Model</title>
+	<title>Action API Improvements Test</title>
 	<icon>device_hub</icon>
 	<defaultRole>true</defaultRole>
 	<anonymousRole>true</anonymousRole>

From cc4fa4774b15af44301afca24897e0704f0f10ab Mon Sep 17 00:00:00 2001
From: palajsamuel <palaj@netgrif.com>
Date: Thu, 23 Apr 2026 14:47:43 +0200
Subject: [PATCH 3/8] [NAE-2390] Action API Improvements

- return value fixed
---
 .../petrinet/domain/dataset/logic/action/ActionDelegate.groovy  | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
index 6d401e02f80..3295f7a5f22 100644
--- a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
+++ b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
@@ -949,7 +949,7 @@ class ActionDelegate {
             return this.findCases([caseRef.value].flatten() as List<String>)
         } catch (ClassCastException e) {
             log.error("Method cannot be used with field with id [${caseRef.importId}].", e)
-            return null
+            return []
         }
     }
 

From 363ae24085f467ecfc0df4394f1e170ae813ec5a Mon Sep 17 00:00:00 2001
From: palajsamuel <palaj@netgrif.com>
Date: Thu, 23 Apr 2026 14:55:52 +0200
Subject: [PATCH 4/8] [NAE-2390] Action API Improvements

- javadoc fixes
---
 .../domain/dataset/logic/action/ActionDelegate.groovy       | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
index 3295f7a5f22..39446967837 100644
--- a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
+++ b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
@@ -925,7 +925,7 @@ class ActionDelegate {
      * current action context see other overloads of this action.
      *
      * <p>If the field value is {@code null}, this method returns an empty list.</p>
-     * <p>If the value cannot be converted to case IDs, this method returns {@code null}.</p>
+     * <p>If the value cannot be converted to case IDs, this method returns an empty list.</p>
      *
      * @param caseRef field whose value contains case IDs, may be of types
      * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#CASE_REF},
@@ -960,7 +960,7 @@ class ActionDelegate {
      * action context see other overloads of this action.
      *
      * <p>If the field value is {@code null}, this method returns an empty list.</p>
-     * <p>If the value cannot be converted to case IDs, this method returns {@code null}.</p>
+     * <p>If the value cannot be converted to case IDs, this method returns an empty list.</p>
      *
      * @param caseRef field whose value contains case IDs, may be of types
      * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#CASE_REF},
@@ -1346,7 +1346,7 @@ class ActionDelegate {
      * Finds tasks by their MongoDB IDs.
      *
      * @param mongoIds task identifiers
-     * @return list of matching tasks, or an empty list when the input is {@code null} org {@code empty}
+     * @return list of matching tasks, or an empty list when the input is {@code null} or {@code empty}
      * @see ActionDelegate#findTasks(Field)
      * @see ActionDelegate#findTasks(DataField)
      */

From fb5c3bc35577f28dbe9cd6243547cb508ec68f10 Mon Sep 17 00:00:00 2001
From: palajsamuel <palaj@netgrif.com>
Date: Tue, 26 May 2026 10:15:54 +0200
Subject: [PATCH 5/8] [NAE-2390] Action API Improvements

- logs in ActionDelegate changed from error to warn level in newly added methods
- ActionDelegateTest readability improved
---
 .../logic/action/ActionDelegate.groovy        |  44 ++---
 .../engine/action/ActionDelegateTest.groovy   | 185 +++++++++---------
 2 files changed, 113 insertions(+), 116 deletions(-)

diff --git a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
index 39446967837..16ca1e9b10b 100644
--- a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
+++ b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
@@ -942,7 +942,7 @@ class ActionDelegate {
      */
     List<Case> findCases(Field caseRef) {
         if(caseRef.value == null) {
-            log.error("Value of field with id [${caseRef.importId}] is null, returning empty list.")
+            log.debug("[findCases(Field)]: Value of field with id [${caseRef.importId}] is null, returning empty list.")
             return []
         }
         try {
@@ -977,7 +977,7 @@ class ActionDelegate {
      */
     List<Case> findCases(DataField caseRef) {
         if(caseRef.value == null) {
-            log.error("Value of field is null, returning empty list.")
+            log.debug("[findCases(DataField]: Value of field is null, returning empty list.")
             return []
         }
         try {
@@ -1001,7 +1001,7 @@ class ActionDelegate {
      */
     List<Case> findCases(List<String> mongoIds) {
         if(mongoIds == null || mongoIds.empty) {
-            log.warn("Null value detected, returning empty list.")
+            log.debug("[findCases(List<String>)]: Null value detected, returning empty list.")
             return []
         }
         return workflowService.findAllById(mongoIds)
@@ -1037,13 +1037,13 @@ class ActionDelegate {
      */
     Case findCase(Field caseRef) {
         if(caseRef.value == null) {
-            log.error("Value of field with id [${caseRef.importId}] is null, returning null.")
+            log.debug("[findCase(Field]: Value of field with id [${caseRef.importId}] is null, returning null.")
             return null
         }
         try {
             List<String> castValue = [caseRef.value].flatten() as List<String>
             if(castValue.size() == 0) {
-                log.error("Value of field with id [${caseRef.importId}] does not contain at least one element, returning null.")
+                log.debug("[findCase(Field]: Value of field with id [${caseRef.importId}] does not contain at least one element, returning null.")
                 return null
             }
             return this.findCase(castValue[0])
@@ -1077,13 +1077,13 @@ class ActionDelegate {
      */
     Case findCase(DataField caseRef) {
         if(caseRef.value == null) {
-            log.error("Value of field is null, returning null.")
+            log.debug("[findCase(DataField)]: Value of field is null, returning null.")
             return null
         }
         try {
             List<String> castValue = [caseRef.value].flatten() as List<String>
             if(castValue.size() == 0) {
-                log.error("Value of field does not contain at least one element, returning null.")
+                log.debug("[findCase(DataField)]: Value of field does not contain at least one element, returning null.")
                 return null
             }
             return this.findCase(castValue[0])
@@ -1124,7 +1124,7 @@ class ActionDelegate {
      */
     Case deleteCase(String mongoId) {
         if(mongoId == null){
-            log.warn("Null value detected, returning null.")
+            log.debug("[deleteCase(String)]: Null value detected, returning null.")
             return null
         }
         return this.deleteCase(workflowService.findOne(mongoId))
@@ -1138,7 +1138,7 @@ class ActionDelegate {
      */
     Case deleteCase(Case toDelete) {
         if(toDelete == null){
-            log.warn("Null value detected, returning null.")
+            log.debug("[deleteCase(Case)]: Null value detected, returning null.")
             return null
         }
         return workflowService.deleteCase(toDelete).case
@@ -1298,7 +1298,7 @@ class ActionDelegate {
      */
     List<Task> findTasks(Field taskRef) {
         if(taskRef.value == null) {
-            log.error("Value of field with id [${taskRef.importId}] is null, returning empty list.")
+            log.debug("[findTasks(Field)]: Value of field with id [${taskRef.importId}] is null, returning empty list.")
             return []
         }
         try {
@@ -1331,7 +1331,7 @@ class ActionDelegate {
      */
     List<Task> findTasks(DataField taskRef) {
         if(taskRef.value == null) {
-            log.error("Value of field is null, returning empty list.")
+            log.debug("[findTasks(DataField)]: Value of field is null, returning empty list.")
             return []
         }
         try {
@@ -1352,7 +1352,7 @@ class ActionDelegate {
      */
     List<Task> findTasks(List<String> mongoIds) {
         if(mongoIds == null || mongoIds.empty) {
-            log.warn("Null value detected, returning empty list.")
+            log.debug("[findTasks(List<String>)]: Null value detected, returning empty list.")
             return []
         }
         return taskService.findAllById(mongoIds)
@@ -1391,13 +1391,13 @@ class ActionDelegate {
      */
     Task findTask(Field taskRef) {
         if(taskRef.value == null) {
-            log.error("Value of field with id [${taskRef.importId}] is null, returning null")
+            log.debug("[findTask(Field)]: Value of field with id [${taskRef.importId}] is null, returning null")
             return null
         }
         try {
             List<String> castValue = [taskRef.value].flatten() as List<String>
             if(castValue.size() == 0) {
-                log.error("Value of field with id [${taskRef.importId}] does not contain at least one element, returning null.")
+                log.debug("[findTask(Field)]: Value of field with id [${taskRef.importId}] does not contain at least one element, returning null.")
                 return null
             }
             return this.findTask(castValue[0])
@@ -1431,13 +1431,13 @@ class ActionDelegate {
      */
     Task findTask(DataField taskRef) {
         if(taskRef.value == null) {
-            log.error("Value of field is null, returning null")
+            log.debug("[findTask(DataField)]: Value of field is null, returning null")
             return null
         }
         try {
             List<String> castValue = [taskRef.value].flatten() as List<String>
             if(castValue.size() == 0) {
-                log.error("Value of field does not contain at least one element, returning null.")
+                log.debug("[findTask(DataField)]: Value of field does not contain at least one element, returning null.")
                 return null
             }
             return this.findTask(castValue[0])
@@ -1455,7 +1455,7 @@ class ActionDelegate {
      */
     PetriNet findPetriNet(String mongoId) {
         if(mongoId == null){
-            log.warn("Null value detected, returning null.")
+            log.debug("[findPetriNet(String)]: Null value detected, returning null.")
             return null
         }
         return petriNetService.getPetriNet(mongoId)
@@ -1469,7 +1469,7 @@ class ActionDelegate {
      */
     PetriNet findPetriNet(ObjectId objectId) {
         if(objectId == null){
-            log.warn("Null value detected, returning null.")
+            log.debug("[findPetriNet(ObjectId)]: Null value detected, returning null.")
             return null
         }
         return petriNetService.get(objectId)
@@ -1483,7 +1483,7 @@ class ActionDelegate {
      */
     List<PetriNet> findPetriNets(List<String> mongoIds) {
         if(mongoIds == null || mongoIds.empty){
-            log.warn("Null value detected, returning empty list.")
+            log.debug("[findPetriNets(List<String>)]: Null value detected, returning empty list.")
             return []
         }
         return petriNetService.findAllById(mongoIds)
@@ -1497,7 +1497,7 @@ class ActionDelegate {
      */
     List<PetriNet> findPetriNetsByObjectIds(List<ObjectId> objectIds) {
         if(objectIds == null || objectIds.empty){
-            log.warn("Null value detected, returning empty list.")
+            log.debug("[findPetriNetsByObjectIds(List<ObjectId>)]: Null value detected, returning empty list.")
             return []
         }
         return petriNetService.get(objectIds as Collection<ObjectId>)
@@ -1514,7 +1514,7 @@ class ActionDelegate {
      */
     PetriNet findPetriNetByIdentifier(String identifier, Version version = null) {
         if(identifier == null) {
-            log.warn("Null identifier value detected, returning null.")
+            log.debug("[findPetriNetByIdentifier(String, Version)]: Null identifier value detected, returning null.")
             return null
         }
         return version == null ? petriNetService.getNewestVersionByIdentifier(identifier) : petriNetService.getPetriNet(identifier, version)
@@ -1536,7 +1536,7 @@ class ActionDelegate {
 
     String getTaskId(String transitionId, Case aCase = useCase) {
         List<TaskReference> refs = taskService.findAllByCase(aCase.stringId, null)
-        refs.find { it.transitionId == transitionId }.stringId
+        return refs.find { it.transitionId == transitionId }.stringId
     }
 
     /**
diff --git a/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy b/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy
index dc10532dbd3..96b2d610d76 100644
--- a/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy
+++ b/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy
@@ -32,9 +32,8 @@ import org.springframework.test.context.junit.jupiter.SpringExtension
 
 import javax.mail.internet.MimeMessage
 
-import static java.util.Base64.*
-import static org.junit.jupiter.api.Assertions.assertThrows
-import static org.junit.jupiter.api.Assertions.assertEquals;
+import static java.util.Base64.getEncoder
+import static org.junit.jupiter.api.Assertions.*
 
 @SpringBootTest
 @ActiveProfiles(["test"])
@@ -143,9 +142,9 @@ class ActionDelegateTest {
         List<String> taskIds = testCase.tasks.collect { it.task }
         List<Task> tasks = actionDelegate.findTasks(taskIds)
 
-        assert tasks != null
-        assert !tasks.empty
-        assert tasks.size() == taskIds.size()
+        assertNotNull(tasks)
+        assertFalse(tasks.empty)
+        assertEquals(tasks.size(), taskIds.size())
 
         testCase = actionDelegate.setData("t1", testCase, [
                 "enumeration_map" : [
@@ -193,14 +192,13 @@ class ActionDelegateTest {
         List<String> caseIds = [testCase1.stringId, testCase2.stringId, testCase3.stringId, testCase4.stringId]
 
         Case searchedCase = actionDelegate.findCase(caseIds[0])
-        assert searchedCase != null
-        assert searchedCase.stringId == testCase1.stringId
+        assertNotNull(searchedCase)
+        assertEquals(searchedCase.stringId, testCase1.stringId)
 
         List<Case> searchedCases = actionDelegate.findCases(caseIds)
-        assert searchedCases != null
-        assert !searchedCases.empty
-        assert searchedCases.size() == caseIds.size()
-
+        assertNotNull(searchedCases)
+        assertFalse(searchedCases.empty)
+        assertEquals(searchedCases.size(), caseIds.size())
 
         testCase1 = actionDelegate.setData("t1", testCase1, [
                 "enumeration_map" : [
@@ -258,56 +256,55 @@ class ActionDelegateTest {
         ObjectId objectId = importedNet.objectId
 
         PetriNet foundNet = actionDelegate.findPetriNet(mongoId)
-        assert foundNet != null
-        assert foundNet.objectId == objectId
-        assert foundNet.identifier == netIdentifier
+        assertNotNull(foundNet)
+        assertEquals(foundNet.objectId, objectId)
+        assertEquals(foundNet.identifier, netIdentifier)
         foundNet = null
 
         foundNet = actionDelegate.findPetriNet(objectId)
-        assert foundNet != null
-        assert foundNet.stringId == mongoId
-        assert foundNet.identifier == netIdentifier
+        assertNotNull(foundNet)
+        assertEquals(foundNet.stringId, mongoId)
+        assertEquals(foundNet.identifier, netIdentifier)
         foundNet = null
 
         PetriNet filterNet = petriNetService.getByIdentifier("filter")[0]
         String mongoId2 = filterNet.stringId
-        String netIdentifier2 = filterNet.getIdentifier()
         ObjectId objectId3 = filterNet.objectId
 
         def searchTargets = [mongoId, mongoId2]
         List<PetriNet> searchResults = actionDelegate.findPetriNets(searchTargets as List<String>)
-        assert searchResults != null
-        assert !searchResults.empty
-        assert searchResults.collect { it.stringId }.containsAll(searchTargets)
+        assertNotNull(searchResults)
+        assertFalse(searchResults.empty)
+        assertTrue(searchResults.collect { it.stringId }.containsAll(searchTargets))
         searchResults = null
 
-        searchTargets = [objectId, objectId]
+        searchTargets = [objectId, objectId3]
         searchResults = actionDelegate.findPetriNetsByObjectIds(searchTargets as List<ObjectId>)
-        assert searchResults != null
-        assert !searchResults.empty
-        assert searchResults.collect { it.objectId }.containsAll(searchTargets)
+        assertNotNull(searchResults)
+        assertFalse(searchResults.empty)
+        assertTrue(searchResults.collect { it.objectId }.containsAll(searchTargets))
 
         PetriNet importedNet2 = importTestPetriNet()
-        assert importedNet2 != null
-        assert importedNet2.identifier == netIdentifier
-        assert importedNet2.stringId != mongoId
-        assert importedNet2.version != importedNet.version
+        assertNotNull(importedNet2)
+        assertEquals(importedNet2.identifier, netIdentifier)
+        assertNotEquals(importedNet2.stringId, mongoId)
+        assertNotEquals(importedNet2.version, importedNet.version)
 
         foundNet = actionDelegate.findPetriNetByIdentifier(importedNet2.identifier, new Version(1, 0, 0))
-        assert foundNet != null
-        assert foundNet.identifier == netIdentifier
-        assert foundNet.stringId == mongoId
-        assert foundNet.version == importedNet.version
-        assert foundNet.version != importedNet2.version
+        assertNotNull(foundNet, null)
+        assertEquals(foundNet.identifier, netIdentifier)
+        assertEquals(foundNet.stringId, mongoId)
+        assertEquals(foundNet.version, importedNet.version)
+        assertNotEquals(foundNet.version, importedNet2.version)
         foundNet = null
 
 //        find newest
         foundNet = actionDelegate.findPetriNetByIdentifier(importedNet2.identifier)
-        assert foundNet != null
-        assert foundNet.identifier == netIdentifier
-        assert foundNet.stringId != mongoId
-        assert foundNet.version != importedNet.version
-        assert foundNet.version == importedNet2.version
+        assertNotNull(foundNet)
+        assertEquals(foundNet.identifier, netIdentifier)
+        assertNotEquals(foundNet.stringId, mongoId)
+        assertNotEquals(foundNet.version, importedNet.version)
+        assertEquals(foundNet.version, importedNet2.version)
     }
 
     @Test
@@ -318,31 +315,31 @@ class ActionDelegateTest {
 
         List<Case> cases = [case1, case2]
         Map<String, I18nString> options = actionDelegate.casesToOptions(cases)
-        assert options != null
-        assert options.size() == cases.size()
-        assert options.keySet().containsAll(cases.collect { it.stringId })
-        assert options.get(case1.stringId).defaultValue == case1.title
-        assert options.get(case2.stringId).defaultValue == case2.title
+        assertNotNull(options)
+        assertEquals(options.size(), cases.size())
+        assertTrue(options.keySet().containsAll(cases.collect { it.stringId }))
+        assertEquals(options.get(case1.stringId).defaultValue, case1.title)
+        assertEquals(options.get(case2.stringId).defaultValue, case2.title)
         options = null
 
         String keyTransformationTestString = "Key transformation test "
         String valueTransformationTestString = "Value transformation test "
         options = actionDelegate.casesToOptions(cases, { return "Value transformation test ".concat(it.title) }, { return "Key transformation test ".concat(it.stringId) })
-        assert options != null
-        assert options.size() == cases.size()
-        assert options.keySet().containsAll(cases.collect { keyTransformationTestString.concat(it.stringId) })
-        assert options.get(keyTransformationTestString.concat(case1.stringId)).defaultValue == valueTransformationTestString.concat(case1.title)
-        assert options.get(keyTransformationTestString.concat(case2.stringId)).defaultValue == valueTransformationTestString.concat(case2.title)
+        assertNotNull(options != null)
+        assertEquals(options.size(), cases.size())
+        assertTrue(options.keySet().containsAll(cases.collect { keyTransformationTestString.concat(it.stringId) }))
+        assertEquals(options.get(keyTransformationTestString.concat(case1.stringId)).defaultValue, valueTransformationTestString.concat(case1.title))
+        assertEquals(options.get(keyTransformationTestString.concat(case2.stringId)).defaultValue, valueTransformationTestString.concat(case2.title))
 
         case1.dataSet["enumeration_map"].options = options
         actionDelegate.useCase = case1
         actionDelegate.initFieldsMap(["enumeration_map": "enumeration_map"])
         EnumerationMapField field = (EnumerationMapField) actionDelegate.map.get("enumeration_map")
-        assert field != null
-        assert field.options != null
+        assertNotNull(field)
+        assertNotNull(field.options)
         I18nString option = field.getOption(keyTransformationTestString.concat(case1.stringId))
-        assert option != null
-        assert option.defaultValue == valueTransformationTestString.concat(case1.title)
+        assertNotNull(option)
+        assertEquals(option.defaultValue, valueTransformationTestString.concat(case1.title))
     }
 
     @Test
@@ -354,30 +351,30 @@ class ActionDelegateTest {
 
 //        testing "byTransition" variants should be enough, as they call methods, that take List<Task> instead of List<String>
         List<Task> tasks = actionDelegate.assignTasksByTransitions(transitionIds, testCase)
-        assert tasks != null
-        assert tasks.size() == transitionIds.size()
-        assert tasks.stream().allMatch { it.user.email == user.email }
-        assert tasks.stream().allMatch { it.userId == user.stringId }
-        assert tasks.stream().allMatch { it.startDate != null }
+        assertNotNull(tasks)
+        assertEquals(tasks.size(), transitionIds.size())
+        assertTrue(tasks.stream().allMatch { it.user.email == user.email })
+        assertTrue(tasks.stream().allMatch { it.userId == user.stringId })
+        assertTrue(tasks.stream().allMatch { it.startDate != null })
         tasks = null
 
         tasks = actionDelegate.cancelTasksByTransitions(transitionIds, testCase)
-        assert tasks != null
-        assert tasks.size() == transitionIds.size()
-        assert tasks.stream().allMatch { it.userId == null }
-        assert tasks.stream().allMatch { it.startDate == null }
+        assertNotNull(tasks)
+        assertEquals(tasks.size(), transitionIds.size())
+        assertTrue(tasks.stream().allMatch { it.userId == null })
+        assertTrue(tasks.stream().allMatch { it.startDate == null })
         tasks = null
 
         actionDelegate.assignTasksByTransitions(transitionIds, testCase)
         tasks = actionDelegate.finishTasksByTransitions(transitionIds, testCase)
-        assert tasks != null
-        assert tasks.size() == transitionIds.size()
-        assert tasks.stream().allMatch { it.finishedBy == user.stringId }
-        assert tasks.stream().allMatch { it.finishDate != null }
-        assert tasks.stream().allMatch { it.userId == null }
+        assertNotNull(tasks)
+        assertEquals(tasks.size(), transitionIds.size())
+        assertTrue(tasks.stream().allMatch { it.finishedBy == user.stringId })
+        assertTrue(tasks.stream().allMatch { it.finishDate != null })
+        assertTrue(tasks.stream().allMatch { it.userId == null })
         tasks = actionDelegate.findTasks(tasks.collect { it.stringId })
-        assert tasks.size() == 1
-        assert tasks[0].transitionId == "t2"
+        assertEquals(tasks.size(), 1)
+        assertEquals(tasks[0].transitionId, "t2")
     }
 
     private void assertCaseDeletion(String deletedCaseId) {
@@ -397,33 +394,33 @@ class ActionDelegateTest {
 
         Task task = actionDelegate.findTask(field)
         String errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findTask(Field)")
-        assert task != null : errorMessage
-        assert task.stringId == firstTaskId : errorMessage
+        assertNotNull(task, errorMessage)
+        assertEquals(task.stringId, firstTaskId, errorMessage)
         task = null
 
         List<Task> tasks = actionDelegate.findTasks(field)
         errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findTasks(Field)")
-        assert tasks != null : errorMessage
-        assert !tasks.empty : errorMessage
-        assert tasks.size() == sizeToCheck : errorMessage
+        assertNotNull(tasks, errorMessage)
+        assertFalse(tasks.empty, errorMessage)
+        assertEquals(tasks.size(), sizeToCheck, errorMessage)
         if (singleValueField) {
-            assert tasks[0].stringId == firstTaskId : errorMessage
+            assertEquals(tasks[0].stringId, firstTaskId, errorMessage)
         }
         tasks = null
 
         DataField dataField = testCase.getDataField(fieldId)
         task = actionDelegate.findTask(dataField)
         errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findTask(DataField)")
-        assert task != null : errorMessage
-        assert task.stringId == firstTaskId : errorMessage
+        assertNotNull(task, errorMessage)
+        assertEquals(task.stringId, firstTaskId, errorMessage)
 
         tasks = actionDelegate.findTasks(dataField)
         errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findTasks(DataField)")
-        assert tasks != null : errorMessage
-        assert !tasks.empty : errorMessage
-        assert tasks.size() == sizeToCheck : errorMessage
+        assertNotNull(tasks, errorMessage)
+        assertFalse(tasks.empty, errorMessage)
+        assertEquals(tasks.size(), sizeToCheck, errorMessage)
         if (singleValueField) {
-            assert tasks[0].stringId == firstTaskId : errorMessage
+            assertEquals(tasks[0].stringId, firstTaskId, errorMessage)
         }
     }
 
@@ -435,17 +432,17 @@ class ActionDelegateTest {
 
         Case searchedCase = actionDelegate.findCase(field)
         String errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findCase(Field)")
-        assert searchedCase != null : errorMessage
-        assert searchedCase.stringId == firstCaseId : errorMessage
+        assertNotNull(searchedCase, errorMessage)
+        assertEquals(searchedCase.stringId, firstCaseId, errorMessage)
         searchedCase = null
 
         List<Case> searchedCases = actionDelegate.findCases(field)
         errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findCases(Field)")
-        assert searchedCases != null : errorMessage
-        assert !searchedCases.empty : errorMessage
-        assert searchedCases.size() == sizeToCheck : errorMessage
+        assertNotNull(searchedCases, errorMessage)
+        assertFalse(searchedCases.empty, errorMessage)
+        assertEquals(searchedCases.size(), sizeToCheck, errorMessage)
         if (singleValueField) {
-            assert searchedCases[0].stringId == firstCaseId : errorMessage
+            assertEquals(searchedCases[0].stringId, firstCaseId, errorMessage)
         }
         searchedCases = null
 
@@ -453,16 +450,16 @@ class ActionDelegateTest {
         DataField dataField = testCase.getDataField(fieldId)
         searchedCase = actionDelegate.findCase(dataField)
         errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findCase(DataField)")
-        assert searchedCase != null: errorMessage
-        assert searchedCase.stringId == firstCaseId: errorMessage
+        assertNotNull(searchedCase, errorMessage)
+        assertEquals(searchedCase.stringId, firstCaseId, errorMessage)
 
         searchedCases = actionDelegate.findCases(dataField)
         errorMessage = errorMessageWithFieldId.replace(TESTED_METHOD_TEMPLATE, "findCases(DataField)")
-        assert searchedCases != null: errorMessage
-        assert !searchedCases.empty: errorMessage
-        assert searchedCases.size() == sizeToCheck: errorMessage
+        assertNotNull(searchedCases, errorMessage)
+        assertFalse(searchedCases.empty, errorMessage)
+        assertEquals(searchedCases.size(), sizeToCheck, errorMessage)
         if (singleValueField) {
-            assert searchedCases[0].stringId == firstCaseId: errorMessage
+            assertEquals(searchedCases[0].stringId, firstCaseId, errorMessage)
         }
     }
 

From eff3e07ae442cbc34d5b8eb0ed021990041c9e73 Mon Sep 17 00:00:00 2001
From: palajsamuel <palaj@netgrif.com>
Date: Tue, 26 May 2026 10:29:29 +0200
Subject: [PATCH 6/8] [NAE-2390] Action API Improvements

- return type consistency fix
---
 .../domain/dataset/logic/action/ActionDelegate.groovy         | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
index 16ca1e9b10b..bada677a27f 100644
--- a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
+++ b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
@@ -1305,7 +1305,7 @@ class ActionDelegate {
             return this.findTasks([taskRef.value].flatten() as List<String>)
         } catch (ClassCastException e) {
             log.error("Method cannot be used with field with id [${taskRef.importId}].", e)
-            return null
+            return []
         }
     }
     
@@ -1338,7 +1338,7 @@ class ActionDelegate {
             return this.findTasks([taskRef.value].flatten() as List<String>)
         } catch (ClassCastException e) {
             log.error("Method cannot be used with field.", e)
-            return null
+            return []
         }
     }
 

From ca8682d5632fd64bcfeaf369d76a7c93f123e9fe Mon Sep 17 00:00:00 2001
From: palajsamuel <palaj@netgrif.com>
Date: Tue, 26 May 2026 11:20:52 +0200
Subject: [PATCH 7/8] [NAE-2390] Action API Improvements

- assertion bug fix
---
 .../netgrif/application/engine/action/ActionDelegateTest.groovy | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy b/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy
index 96b2d610d76..be9760f1c6b 100644
--- a/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy
+++ b/src/test/groovy/com/netgrif/application/engine/action/ActionDelegateTest.groovy
@@ -325,7 +325,7 @@ class ActionDelegateTest {
         String keyTransformationTestString = "Key transformation test "
         String valueTransformationTestString = "Value transformation test "
         options = actionDelegate.casesToOptions(cases, { return "Value transformation test ".concat(it.title) }, { return "Key transformation test ".concat(it.stringId) })
-        assertNotNull(options != null)
+        assertNotNull(options)
         assertEquals(options.size(), cases.size())
         assertTrue(options.keySet().containsAll(cases.collect { keyTransformationTestString.concat(it.stringId) }))
         assertEquals(options.get(keyTransformationTestString.concat(case1.stringId)).defaultValue, valueTransformationTestString.concat(case1.title))

From 157d2bed83d50bd62d75fa2a17ed01d92d754985 Mon Sep 17 00:00:00 2001
From: palajsamuel <palaj@netgrif.com>
Date: Tue, 26 May 2026 11:31:18 +0200
Subject: [PATCH 8/8] [NAE-2390] Action API Improvements

- javadoc fix
---
 .../domain/dataset/logic/action/ActionDelegate.groovy         | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
index bada677a27f..f29328cc101 100644
--- a/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
+++ b/src/main/groovy/com/netgrif/application/engine/petrinet/domain/dataset/logic/action/ActionDelegate.groovy
@@ -1283,7 +1283,7 @@ class ActionDelegate {
      * current action context see other overloads of this action.
      *
      * <p>If the field value is {@code null}, this method returns an empty list.</p>
-     * <p>If the value cannot be converted to task IDs, this method returns {@code null}.</p>
+     * <p>If the value cannot be converted to task IDs, this method returns an empty list.</p>
      *
      * @param taskRef field whose value contains task IDs
      * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TASK_REF},
@@ -1316,7 +1316,7 @@ class ActionDelegate {
      * current action context see other overloads of this action.
      *
      * <p>If the field value is {@code null}, this method returns an empty list.</p>
-     * <p>If the value cannot be converted to task IDs, this method returns {@code null}.</p>
+     * <p>If the value cannot be converted to task IDs, this method returns an empty list.</p>
      *
      * @param taskRef field whose value contains task IDs
      * {@link com.netgrif.application.engine.petrinet.domain.dataset.FieldType#TASK_REF},
