Repository: MAIF/arta

Issue:
Using Arta as a simple process execution engine
### Is your feature request related to a problem? Please describe

Executing many actions in a same rule group is not possible for now. Only one action can be executed/activated by rule group (when its conditions are `True`).

Doing so will let Arta executing simple workflows.

### Describe the solution you'd like
```python
result = eng.apply_rules(input_data, activation_mode=“many_by_group”)
```

> [!NOTE]
> `activation_mode` is set to `“one_by_group”` by default (rules engine regular behaviour).

#### Illustrations

![image](https://github.com/user-attachments/assets/cdd24f20-3680-4d40-a1f0-2257518c0613)

---

![image](https://github.com/user-attachments/assets/39618618-f133-4c65-83d2-790df1a81681)


### Describe alternatives you've considered
Using a different rule group for each actions but it is verbose.

Discussion:

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 26ff3d3..2a2bd6e 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -26,7 +26,7 @@ repos:
       - id: check-added-large-files
         args: [--maxkb=500]
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.8.0
+    rev: v0.8.2
     hooks:
       - id: ruff
         args: [--fix]
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 944b29f..3b25f1f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,15 +4,25 @@ All notable changes to this project will be documented in this file.
 
 The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
 
-## [0.8.2] - November, 2024
+## [0.9.0] - December, 2024
+
+### Features
+
+* Add a new configuration setting for rule execution: `rule_activation_mode` (#38).
 
 ### Maintenance
 
+* Compatibility with Python 3.13.
 * Use true Pydantic V2 (or Pydantic V1) models (`DeprecationWarning` added about Pydantic V1).
 
+> [!IMPORTANT]
+> **Arta** + **Pydantic V1** + **Python 3.13** is not supported because Pydantic V1 is not supported for Python > 3.12 ([issue 9663](https://github.com/pydantic/pydantic/issues/9663)).
+
 ### Documentation
 
-* New page: *"Use your business objects".*
+* New pages:
+    * *Use your business objects*
+    * *Rule activation mode*
 
 ### Breaking changes
 
diff --git a/docs/mkdocs.yaml b/docs/mkdocs.yaml
index 23a1de4..597613d 100644
--- a/docs/mkdocs.yaml
+++ b/docs/mkdocs.yaml
@@ -83,10 +83,12 @@ nav:
     - How to: how_to.md
     - Glossary: glossary.md
   - Advanced User Guide: 
+    - API Reference: api_reference.md
+    - Custom conditions: custom_conditions.md
     - Parameters: parameters.md
+    - Rule activation mode: rule_activation_mode.md
     - Rule sets: rule_sets.md
     - Use your business objects: business_objects.md
-    - Custom conditions: custom_conditions.md
-    - API Reference: api_reference.md
+    
 extra_css:
   - assets/css/mkdocs_extra.css
\ No newline at end of file
diff --git a/docs/pages/home.md b/docs/pages/home.md
index 23abc2c..372288d 100644
--- a/docs/pages/home.md
+++ b/docs/pages/home.md
@@ -5,26 +5,35 @@
     <em>An Open Source Rules Engine - Make rule handling simple</em>
 </p>
 <p align="center">
-    <img src="https://img.shields.io/pypi/v/arta" alt="Versions">
+    <a href="https://pypi.org/project/arta/"><img src="https://img.shields.io/pypi/v/arta" alt="Versions"></a>
 </p>
 
 # Welcome to the documentation
 
-* Want to discover what is **Arta**? :arrow_right: [Get Started](a_simple_example.md)
-* Want to know how to use it? :arrow_right: [User Guide](how_to.md)
+Want to discover what is **Arta**? :arrow_right: [Get Started](a_simple_example.md)
 
-!!! info "New feature"
 
-    Check out the new and very convenient feature called the [simple condition](how_to.md#simple-condition). A new and lightweight way of configuring your rules' conditions.
+Want to know how to use it? :arrow_right: [User Guide](how_to.md)
 
-**Arta** is automatically tested with:
 
-![Alt Python](https://img.shields.io/pypi/pyversions/arta)
+!!! info inline "New feature"
+
+    Use **Arta** as a *process execution engine* :zap:
+    
+    Read [this page](rule_activation_mode.md) for more details.
 
 !!! tip "Releases"
     
-    Want to see last updates, check the [Release notes](https://github.com/MAIF/arta/releases) :rocket:
+    Check the [Release notes](https://github.com/MAIF/arta/releases) :rocket:
+
+!!! warning "Pydantic 1 compatibility is deprecated"
+
+    **Arta** is working with [Pydantic 2](https://docs.pydantic.dev/latest/) and Pydantic 1 but compatibility with V1  will be removed in the next **major** release.
+
+**Arta** is working and automatically tested with:
+
+![Alt Python](https://img.shields.io/pypi/pyversions/arta)
 
-!!! success "Pydantic 2"
+You like **Arta**? Add a :star:
 
-    **Arta** is now working with [Pydantic 2](https://docs.pydantic.dev/latest/)! And of course, Pydantic 1 as well.
+[![GitHub Repo stars](https://img.shields.io/github/stars/maif/arta)](https://github.com/MAIF/arta)
diff --git a/docs/pages/how_to.md b/docs/pages/how_to.md
index d25a534..6dd8552 100644
--- a/docs/pages/how_to.md
+++ b/docs/pages/how_to.md
@@ -2,10 +2,6 @@ Ensure that you have correctly installed **Arta** before, check the [Installatio
 
 ## Simple condition
 
-!!! beta "Beta feature"
-
-    **Simple condition** is still a *beta feature*, some cases could not work as designed.
-
 **Simple conditions** are a new and straightforward way of configuring your *conditions*.
 
 It simplifies your rules a lot by:
diff --git a/docs/pages/rule_activation_mode.md b/docs/pages/rule_activation_mode.md
new file mode 100644
index 0000000..1d2525c
--- /dev/null
+++ b/docs/pages/rule_activation_mode.md
@@ -0,0 +1,95 @@
+!!! example "Beta feature"
+
+    This new feature (i.e., `rule_activation_mode: many_by_group`) is a **beta feature**, some cases could not work as designed. Please report them using [issues](https://github.com/MAIF/arta/issues).
+
+This feature was designed at MAIF when the idea to use **Arta** as a simple *process execution engine* came about. 
+
+Our goal was to handle different *rules* of data processing inside an ETL pipeline.
+
+It's actually quite simple :zap:
+
+## Illustration
+
+Traditionaly, **Arta** is evaluating rules (like most rules engines) like this:
+
+```mermaid
+---
+title: one_by_group
+---
+flowchart
+  s((Start))
+  e((End))
+  subgraph Group_1
+    r1(Rule 1, conditions False)-->r2(Rule 2, conditions True)-.not evaluated.->r3(Rule 3, conditions True)   
+  end
+    r2-.execute.->a2(Action A)   
+  subgraph Group_2
+    r1b(Rule 1, conditions True)-.not evaluated.->r2b(Rule 2, conditions False)-.not evaluated.->r3b(Rule 3, conditions True)
+  end
+  s-->r1
+  r1b-.execute.->a1b(Action B)
+  r2-->r1b  
+  r3-.->r1b  
+  r1b-->e
+  r3b-.->e
+```
+
+> **Only one rule is activated (i.e., meaning one action is triggered) by rule group.**
+
+---
+
+But if we need to use **Arta** to execute *simple workflows*, we need a *control flow* like this one:
+
+```mermaid
+---
+title: many_by_group
+---
+flowchart
+  s((Start))
+  e((End))
+  subgraph Group_1
+    r1(Rule 1, conditions False)-->r2(Rule 2, conditions True)-->r3(Rule 3, conditions True)   
+  end
+    r2-.execute.->a2(Action A)
+    r3-.execute.->a3(Action B)        
+  subgraph Group_2
+    r1b(Rule 1, conditions True)-->r2b(Rule 2, conditions False)-->r3b(Rule 3, conditions True)
+  end
+  s-->r1
+  r1b-.execute.->a1b(Action C)
+  r3b-.execute.->a3b(Action D)    
+  r3-->r1b
+  r3b-->e
+```
+
+> **All rules are evaluated.**
+
+> **Therefore, many rules can be activated (i.e., meaning many actions can be triggered) by rule group.**
+
+---
+
+## Setting
+
+You just need to add somewhere in the YAML configuration file of **Arta** the following setting:
+
+### One by group
+
+This *traditional* flow of control is the **default one**:
+
+```yaml
+rule_activation_mode: one_by_group
+```
+
+!!! note "Default value"
+
+    Because it is the **default value**, it is *useless* to add this line in the configuration.
+
+### Many by group
+
+This is the *flow of control* of a **process execution engine**:
+
+```yaml
+rule_activation_mode: many_by_group
+```
+
+That's all! You are all set :+1:
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index 221cd13..591ef95 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 
 [project]
 name = "arta"
-version = "0.8.2"
+version = "0.9.0"
 requires-python = ">3.8.0"
 description = "A Python Rules Engine - Make rule handling simple"
 readme = "README.md"
@@ -28,6 +28,7 @@ classifiers = [
     "Programming Language :: Python :: 3.10",
     "Programming Language :: Python :: 3.11",
     "Programming Language :: Python :: 3.12",
+    "Programming Language :: Python :: 3.13",
     "License :: OSI Approved :: Apache Software License",
 ]
 
@@ -42,12 +43,10 @@ Documentation = "https://maif.github.io/arta/home/"
 Repository = "https://github.com/MAIF/arta"
 
 [project.optional-dependencies]
-all = ["arta[test,dev,doc,mypy,ruff]"]
+all = ["arta[test,dev,doc]"]
 test = ["pytest", "tox", "pytest-cov"]
 dev = ["mypy", "pre-commit", "ruff"]
 doc = ["mkdocs-material", "mkdocstrings[python]"]
-mypy = ["mypy"]
-ruff = ["ruff"]
 
 [tool.setuptools]
 package-dir = {"" = "src"}
diff --git a/src/arta/_engine.py b/src/arta/_engine.py
index 85ea1a2..487f0f4 100644
--- a/src/arta/_engine.py
+++ b/src/arta/_engine.py
@@ -16,7 +16,7 @@ from arta.condition import BaseCondition, SimpleCondition, StandardCondition
 from arta.config import load_config
 from arta.models import Configuration, RulesDict
 from arta.rule import Rule
-from arta.utils import ParsingErrorStrategy
+from arta.utils import ParsingErrorStrategy, RuleActivationMode
 
 
 class RulesEngine:
@@ -81,8 +81,10 @@ class RulesEngine:
                 "RulesEngine takes one (and only one) parameter: 'rules_dict' or 'config_path' or 'config_dict'."
             )
 
-        # Init. default parsing_error_strategy (probably not needed because already defined elsewhere)
+        # Init. default global settings (useful if not set, can't be set in the Pydantic model
+        # because of the rules dict mode)
         self._parsing_error_strategy: ParsingErrorStrategy = ParsingErrorStrategy.RAISE
+        self._rule_activation_mode: RuleActivationMode = RuleActivationMode.ONE_BY_GROUP
 
         # Initialize directly with a rules dict
         if rules_dict is not None:
@@ -112,6 +114,10 @@ class RulesEngine:
                 # Set parsing error handling strategy from config
                 self._parsing_error_strategy = ParsingErrorStrategy(config.parsing_error_strategy)
 
+            if config.rule_activation_mode is not None:
+                # Set rule activation mode from config
+                self._rule_activation_mode = RuleActivationMode(config.rule_activation_mode)
+
             # dict of available action functions (k: function name, v: function object)
             action_modules: list[str] = config.actions_source_modules
             action_functions: dict[str, Callable] = self._get_object_from_source_modules(action_modules)
@@ -166,7 +172,8 @@ class RulesEngine:
         """Apply the rules and return results.
 
         For each rule group of a given rule set, rules are applied sequentially,
-        The loop is broken when a rule is applied (an action is triggered).
+        The loop is broken when a rule is applied (an action is triggered)
+        or not (depends on the rule activation mode).
         Then, the next rule group is evaluated.
         And so on...
 
@@ -241,8 +248,9 @@ class RulesEngine:
                     # Update input data with current result with key 'output' (can be used in next rules)
                     input_data_copy["output"][group_id] = copy.deepcopy(results_dict[group_id])
 
-                    # We can only have one result per group => break when "action_result" in rule_details
-                    break
+                    if self._rule_activation_mode is RuleActivationMode.ONE_BY_GROUP:
+                        # We can only have one result per group => break when "action_result" in rule_details
+                        break
 
         # Handling non-verbose mode
         if not verbose:
diff --git a/src/arta/condition.py b/src/arta/condition.py
index 2355738..a6ad40a 100644
--- a/src/arta/condition.py
+++ b/src/arta/condition.py
@@ -183,10 +183,12 @@ class SimpleCondition(BaseCondition):
         path_matches: list[str] = re.findall(data_path_patt, unitary_expr)
 
         if len(path_matches) > 0:
+            locals_ns: dict[str, Any] = {}
+
             # Regular case: we have a data paths
             for idx, path in enumerate(path_matches):
                 # Read data from the path
-                locals()[f"data_{idx}"] = parse_dynamic_parameter(  # noqa
+                locals_ns[f"data_{idx}"] = parse_dynamic_parameter(
                     parameter=path, input_data=input_data, parsing_error_strategy=parsing_error_strategy
                 )
 
@@ -195,7 +197,7 @@ class SimpleCondition(BaseCondition):
 
             # Evaluate the expression
             try:
-                bool_var = eval(unitary_expr)  # noqa
+                bool_var = eval(unitary_expr, None, locals_ns)  # noqa
             except TypeError:
                 # Ignore evaluation --> False
                 pass
diff --git a/src/arta/models.py b/src/arta/models.py
index 05d578d..20669bc 100644
--- a/src/arta/models.py
+++ b/src/arta/models.py
@@ -8,7 +8,7 @@ from warnings import warn
 import pydantic
 from pydantic.version import VERSION
 
-from arta.utils import ParsingErrorStrategy
+from arta.utils import ParsingErrorStrategy, RuleActivationMode
 
 PYDANTIC_V1: bool = VERSION.startswith("1.")
 
@@ -66,6 +66,7 @@ if not PYDANTIC_V1:
         condition_factory_mapping: Optional[dict[str, str]] = None
         rules: dict[str, dict[str, dict[Annotated[str, pydantic.StringConstraints(to_upper=True)], RulesConfig]]]
         parsing_error_strategy: Optional[ParsingErrorStrategy] = None
+        rule_activation_mode: Optional[RuleActivationMode] = None
 
 else:
     # Pydantic V1
@@ -141,4 +142,5 @@ else:
         custom_classes_source_modules: Optional[list[str]]
         condition_factory_mapping: Optional[dict[str, str]]
         rules: dict[str, dict[str, dict[pydantic.constr(to_upper=True), RulesConfig]]]  # type: ignore
-        parsing_error_strategy: Optional[ParsingErrorStrategy]
+        parsing_error_strategy: Optional[ParsingErrorStrategy] = None
+        rule_activation_mode: Optional[RuleActivationMode] = None
diff --git a/src/arta/utils.py b/src/arta/utils.py
index 2552707..ef13b46 100644
--- a/src/arta/utils.py
+++ b/src/arta/utils.py
@@ -16,6 +16,16 @@ class ParsingErrorStrategy(str, Enum):
     DEFAULT_VALUE: str = "default_value"
 
 
+class RuleActivationMode(str, Enum):
+    """Define how Arta is processing rules.
+
+    ONE_BY_GROUP is the default mode.
+    """
+
+    ONE_BY_GROUP: str = "one_by_group"
+    MANY_BY_GROUP: str = "many_by_group"
+
+
 def get_value_in_nested_dict_from_path(path: str, nested_dict: dict[str, Any]) -> Any:
     """From a path, get a value in a nested dict.
 
diff --git a/tox.ini b/tox.ini
index e34568e..64decca 100644
--- a/tox.ini
+++ b/tox.ini
@@ -1,6 +1,7 @@
 [tox]
 min_version = 4.0
 env_list =
+    py313
     py312
     py311
     py310


Test patch:
diff --git a/tests/examples/code/actions.py b/tests/examples/code/actions.py
index 9a4be3e..0dae41f 100644
--- a/tests/examples/code/actions.py
+++ b/tests/examples/code/actions.py
@@ -30,7 +30,7 @@ def send_email(mail_to: str, mail_content: str, meal: str, **kwargs: Any) -> boo
     return is_ok
 
 
-def concatenate_str(list_str: list[Any], **kwargs: Any) -> str:
+def concatenate_list(list_str: list[Any], **kwargs: Any) -> str:
     """Demo function: return the concatenation of a list of string using input_data (two levels max)."""
     list_str = [str(element) for element in list_str]
     return "".join(list_str)
@@ -44,3 +44,8 @@ def do_nothing(**kwargs: Any) -> None:
 def compute_sum(value1: float, value2: float, **kwargs: Any) -> float:
     """Demo function: return sum of two values."""
     return value1 + value2
+
+
+def concatenate(value1: str, value2: str, **kwargs: Any) -> str:
+    """Demo function: return the concatenation of two strings."""
+    return value1 + value2
diff --git a/tests/examples/failing_conf/wrong_rule_activation_mode/wrong_rule_activation_mode.yaml b/tests/examples/failing_conf/wrong_rule_activation_mode/wrong_rule_activation_mode.yaml
new file mode 100644
index 0000000..9fd37c0
--- /dev/null
+++ b/tests/examples/failing_conf/wrong_rule_activation_mode/wrong_rule_activation_mode.yaml
@@ -0,0 +1,47 @@
+---
+rule_activation_mode: dummy-value
+
+actions_source_modules:
+  - tests.examples.code.actions
+
+rules:
+  default_rule_set:
+    rg_1:
+      RULE_1:
+        simple_condition: input.power=="strength" or input.power=="fly" or input.power=="time-manipulation"
+        action: concatenate
+        action_parameters:
+          value1: ""
+          value2: a
+      RULE_2:
+        simple_condition: null
+        action: concatenate
+        action_parameters:
+          value1: ""
+          value2: b
+    rg_2:
+      RULE_1:
+        simple_condition: input.language=="english" and input.age!=None
+        action: concatenate
+        action_parameters:
+          value1: output.rg_1
+          value2: c
+      RULE_2:
+        simple_condition: input.age>=100 or input.age==None
+        action: concatenate
+        action_parameters:
+          value1: output.rg_1
+          value2: d
+      RULE_3:
+        simple_condition: input.language=="french"
+        action: concatenate
+        action_parameters:
+          value1: output.rg_1
+          value2: e
+    rg_3:
+      RULE_1:
+        simple_condition: input.favorite_meal!=None
+        action: concatenate
+        action_parameters:
+          value1: output.rg_2
+          value2: f
diff --git a/tests/examples/ignore_conf/rules.yaml b/tests/examples/ignore_conf/rules.yaml
index 94d7486..d4d6750 100644
--- a/tests/examples/ignore_conf/rules.yaml
+++ b/tests/examples/ignore_conf/rules.yaml
@@ -5,7 +5,7 @@ rules:
     test_action:
       TEST_IGNORE:
         condition: null
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
           - "My name is "
diff --git a/tests/examples/process_execution/simple_process.yaml b/tests/examples/process_execution/simple_process.yaml
new file mode 100644
index 0000000..4f5087d
--- /dev/null
+++ b/tests/examples/process_execution/simple_process.yaml
@@ -0,0 +1,41 @@
+---
+rule_activation_mode: many_by_group  # default is 'one_by_group'
+
+actions_source_modules:
+  - tests.examples.code.actions
+
+rules:
+  default_rule_set:
+    gr_1:
+      TASK_1:
+        action: concatenate
+        action_parameters:
+          value1: a
+          value2: ""
+      TASK_2:
+        action: concatenate
+        action_parameters:
+          value1: output.gr_1
+          value2: b
+    gr_2:
+      TASK_1:
+        action: concatenate
+        action_parameters:
+          value1: output.gr_1
+          value2: c
+      TASK_2:
+        action: concatenate
+        action_parameters:
+          value1: output.gr_2
+          value2: d
+      TASK_3:
+        action: concatenate
+        action_parameters:
+          value1: output.gr_2
+          value2: e
+    gr_3:
+      TASK_1:
+        action: concatenate
+        action_parameters:
+          value1: output.gr_2
+          value2: f
diff --git a/tests/examples/rule_activation_mode/rules_many_by_group.yaml b/tests/examples/rule_activation_mode/rules_many_by_group.yaml
new file mode 100644
index 0000000..f42184e
--- /dev/null
+++ b/tests/examples/rule_activation_mode/rules_many_by_group.yaml
@@ -0,0 +1,47 @@
+---
+rule_activation_mode: many_by_group  # default is 'one_by_group'
+
+actions_source_modules:
+  - tests.examples.code.actions
+
+rules:
+  default_rule_set:
+    rg_1:
+      RULE_1:
+        simple_condition: input.power=="strength" or input.power=="fly" or input.power=="time-manipulation"
+        action: concatenate
+        action_parameters:
+          value1: ""
+          value2: a
+      RULE_2:
+        simple_condition: null
+        action: concatenate
+        action_parameters:
+          value1: ""
+          value2: b
+    rg_2:
+      RULE_1:
+        simple_condition: input.language=="english" and input.age!=None
+        action: concatenate
+        action_parameters:
+          value1: output.rg_1
+          value2: c
+      RULE_2:
+        simple_condition: input.age>=100 or input.age==None
+        action: concatenate
+        action_parameters:
+          value1: output.rg_1
+          value2: d
+      RULE_3:
+        simple_condition: input.language=="french"
+        action: concatenate
+        action_parameters:
+          value1: output.rg_1
+          value2: e
+    rg_3:
+      RULE_1:
+        simple_condition: input.favorite_meal!=None
+        action: concatenate
+        action_parameters:
+          value1: output.rg_2
+          value2: f
diff --git a/tests/examples/simple_condition/math/rules_simpl_cond_math.yaml b/tests/examples/simple_condition/math/rules_simpl_cond_math.yaml
index 0167666..fa8cc4a 100644
--- a/tests/examples/simple_condition/math/rules_simpl_cond_math.yaml
+++ b/tests/examples/simple_condition/math/rules_simpl_cond_math.yaml
@@ -9,7 +9,7 @@ rules:
     add:
       GREATER:
         simple_condition: input.a+input.b>input.threshold
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - greater
@@ -17,7 +17,7 @@ rules:
             - threshold
       LESS:
         simple_condition: input.a+input.b<=input.threshold
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - less or equal
@@ -26,7 +26,7 @@ rules:
     sub:
       GREATER:
         simple_condition: input.a-input.b>input.threshold
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - greater
@@ -34,7 +34,7 @@ rules:
             - threshold
       LESS:
         simple_condition: input.a-input.b<=input.threshold
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - less or equal
@@ -43,7 +43,7 @@ rules:
     mul:
       GREATER:
         simple_condition: input.a*input.b>input.threshold
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - greater
@@ -51,7 +51,7 @@ rules:
             - threshold
       LESS:
         simple_condition: input.a*input.b<=input.threshold
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - less or equal
@@ -60,7 +60,7 @@ rules:
     div:
       GREATER:
         simple_condition: input.a/input.b>input.threshold
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - greater
@@ -68,7 +68,7 @@ rules:
             - threshold
       LESS:
         simple_condition: input.a/input.b<=input.threshold
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - less or equal
@@ -77,26 +77,26 @@ rules:
     equal_1:
       "YES":
         simple_condition: input.a==input.b
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - "yes"
       "NO":
         simple_condition: input.a!=input.b
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - "no"
     equal_2:
       "YES":
         simple_condition: input.a==1.3
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - "yes"
       "NO":
         simple_condition: input.a!=1.3
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - "no"
diff --git a/tests/examples/simple_condition/uppercase/rules_simple_cond_uppercase.yaml b/tests/examples/simple_condition/uppercase/rules_simple_cond_uppercase.yaml
index ddf4458..b1b47a6 100644
--- a/tests/examples/simple_condition/uppercase/rules_simple_cond_uppercase.yaml
+++ b/tests/examples/simple_condition/uppercase/rules_simple_cond_uppercase.yaml
@@ -9,19 +9,19 @@ rules:
     uppercase:
       RULE_1:
         simple_condition: input.text=="SUPERHERO" or input.text=="SUPER HERO" or input.text=="SUPER_HERO" or input.text=="SUPER-HERO"
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - OK
     camelcase:
       RULE_1:
         simple_condition: input.streetNumber>0 and input.streetName!="" and input.PostalCode>0
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - OK
       RULE_2:
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - KO
diff --git a/tests/examples/simple_condition/whitespace/rules_simple_cond_whitespace.yaml b/tests/examples/simple_condition/whitespace/rules_simple_cond_whitespace.yaml
index d609d69..c60eddc 100644
--- a/tests/examples/simple_condition/whitespace/rules_simple_cond_whitespace.yaml
+++ b/tests/examples/simple_condition/whitespace/rules_simple_cond_whitespace.yaml
@@ -9,7 +9,7 @@ rules:
     whitespace:
       RULE_1:
         simple_condition: input.text=="super hero super hero"
-        action: concatenate_str
+        action: concatenate_list
         action_parameters:
           list_str:
             - OK
diff --git a/tests/test_example_code/test_actions.py b/tests/test_example_code/test_actions.py
index f05c98e..442e003 100644
--- a/tests/test_example_code/test_actions.py
+++ b/tests/test_example_code/test_actions.py
@@ -49,9 +49,9 @@ def test_send_email(mail_to, mail_content, meal, expected):
     assert result == expected
 
 
-def test_concatenate_str():
+def test_concatenate_list():
     """Action function unit test."""
-    result = actions.concatenate_str(["a", "b", "c"])
+    result = actions.concatenate_list(["a", "b", "c"])
     assert result == "abc"
 
 
@@ -65,3 +65,9 @@ def test_compute_sum():
     """Action function unit test."""
     result = actions.compute_sum(2, 3)
     assert result == 5
+
+
+def test_concatenate():
+    """Action function unit test."""
+    result = actions.concatenate("a", "b")
+    assert result == "ab"
diff --git a/tests/unit/test_engine_errors.py b/tests/unit/test_engine_errors.py
index 9893cd4..fa3273a 100644
--- a/tests/unit/test_engine_errors.py
+++ b/tests/unit/test_engine_errors.py
@@ -149,6 +149,11 @@ import pydantic
             "failing_conf/wrong_parsing_error_strategy/",
             pydantic.ValidationError,
         ),
+        (
+            None,
+            "failing_conf/wrong_rule_activation_mode/",
+            pydantic.ValidationError,
+        ),
     ],
 )
 def test_instance_error(rules_dict, config_dir, expected_error, base_config_path):
diff --git a/tests/unit/test_rule_activation_mode.py b/tests/unit/test_rule_activation_mode.py
new file mode 100644
index 0000000..6dd780c
--- /dev/null
+++ b/tests/unit/test_rule_activation_mode.py
@@ -0,0 +1,57 @@
+"""UT of the rule activation mode."""
+
+import os
+
+import pytest
+
+from arta import RulesEngine
+
+
+@pytest.mark.parametrize(
+    "input_data, config_dir, good_results",
+    [
+        (
+            {
+                "age": 100,
+                "language": "french",
+                "power": "strength",
+                "favorite_meal": "Spinach",
+            },
+            "rule_activation_mode",
+            {
+                "rg_1": "b",
+                "rg_2": "be",
+                "rg_3": "bef",
+            },
+        ),
+        (
+            {
+                "age": None,
+                "language": "english",
+                "power": "strength",
+                "favorite_meal": None,
+            },
+            "rule_activation_mode",
+            {
+                "rg_1": "b",
+                "rg_2": "bd",
+                "rg_3": None,
+            },
+        ),
+        (
+            {"key": "value"},
+            "process_execution",
+            {
+                "gr_1": "ab",
+                "gr_2": "abcde",
+                "gr_3": "abcdef",
+            },
+        ),
+    ],
+)
+def test_many_by_group(input_data, config_dir, good_results, base_config_path):
+    """Unit test of the regular case"""
+    config_path = os.path.join(base_config_path, config_dir)
+    eng = RulesEngine(config_path=config_path)
+    res = eng.apply_rules(input_data=input_data)
+    assert res == good_results