From 1a11d5a21cdf67d7e0fd0cd616c3abd8b8f927b0 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=A9r=C3=A9mie=20Pardou?= <jeremie@baserow.io>
Date: Tue, 18 Feb 2025 16:28:09 +0000
Subject: [PATCH] Final fix for workflow action with collection elements

---
 .../builder/api/workflow_actions/views.py     |  1 -
 .../data_providers/data_provider_types.py     |  7 ++-
 .../data_sources/builder_dispatch_context.py  | 18 +++++++
 .../contrib/builder/elements/mixins.py        |  8 +++
 .../local_baserow/service_types.py            |  3 ++
 .../baserow/core/services/dispatch_context.py |  7 +++
 .../src/baserow/core/services/registries.py   | 46 ++++++++--------
 .../data_sources/test_data_source_views.py    |  4 ++
 .../data_sources/test_public_domain_views.py  | 11 ++--
 .../api/domains/test_domain_public_views.py   | 20 ++++---
 .../test_workflow_actions_views.py            | 54 +++++++------------
 .../test_data_provider_types.py               | 14 ++---
 .../elements/test_repeat_element_type.py      | 12 +++--
 .../test_formula_property_extractor.py        | 10 ++--
 .../elements/baseComponents/ABTable.vue       |  3 +-
 .../elements/components/BaserowTable.vue      |  2 +
 .../elements/components/RepeatElement.vue     | 26 ++++-----
 .../elements/components/TableElement.vue      | 17 +++---
 .../modules/builder/dataProviderTypes.js      |  5 +-
 web-frontend/modules/builder/eventTypes.js    | 13 -----
 .../builder/mixins/collectionElement.js       | 17 ++++++
 .../builder/services/workflowAction.js        |  8 +--
 .../modules/builder/store/elementContent.js   |  5 +-
 .../modules/builder/store/workflowAction.js   |  9 +---
 .../modules/builder/workflowActionTypes.js    |  2 -
 web-frontend/modules/core/serviceTypes.js     | 16 ++++++
 .../modules/integrations/serviceTypes.js      |  8 +--
 27 files changed, 203 insertions(+), 143 deletions(-)

diff --git a/backend/src/baserow/contrib/builder/api/workflow_actions/views.py b/backend/src/baserow/contrib/builder/api/workflow_actions/views.py
index 8d7ee3264..bb1eb4c3d 100644
--- a/backend/src/baserow/contrib/builder/api/workflow_actions/views.py
+++ b/backend/src/baserow/contrib/builder/api/workflow_actions/views.py
@@ -396,7 +396,6 @@ class DispatchBuilderWorkflowActionView(APIView):
         dispatch_context = BuilderDispatchContext(
             request,
             workflow_action.page,
-            element=workflow_action.element,
             workflow_action=workflow_action,
         )
 
diff --git a/backend/src/baserow/contrib/builder/data_providers/data_provider_types.py b/backend/src/baserow/contrib/builder/data_providers/data_provider_types.py
index 23689d00a..400baa3b4 100644
--- a/backend/src/baserow/contrib/builder/data_providers/data_provider_types.py
+++ b/backend/src/baserow/contrib/builder/data_providers/data_provider_types.py
@@ -298,7 +298,9 @@ class CurrentRecordDataProviderType(DataProviderType):
         """
 
         try:
-            current_record = dispatch_context.request.data["current_record"]
+            current_record_data = dispatch_context.request.data["current_record"]
+            current_record = current_record_data["index"]
+            current_record_id = current_record_data["record_id"]
         except KeyError:
             return None
 
@@ -318,8 +320,9 @@ class CurrentRecordDataProviderType(DataProviderType):
         # Narrow down our range to just our record index.
         dispatch_context = dispatch_context.from_context(
             dispatch_context,
-            offset=current_record,
+            offset=0,
             count=1,
+            only_record_id=current_record_id,
         )
 
         return DataSourceDataProviderType().get_data_chunk(
diff --git a/backend/src/baserow/contrib/builder/data_sources/builder_dispatch_context.py b/backend/src/baserow/contrib/builder/data_sources/builder_dispatch_context.py
index 28402be09..1b30f0bc7 100644
--- a/backend/src/baserow/contrib/builder/data_sources/builder_dispatch_context.py
+++ b/backend/src/baserow/contrib/builder/data_sources/builder_dispatch_context.py
@@ -33,6 +33,7 @@ class BuilderDispatchContext(DispatchContext):
         "element",
         "offset",
         "count",
+        "only_record_id",
         "only_expose_public_allowed_properties",
     ]
 
@@ -44,12 +45,29 @@ class BuilderDispatchContext(DispatchContext):
         element: Optional["Element"] = None,
         offset: Optional[int] = None,
         count: Optional[int] = None,
+        only_record_id: Optional[int | str] = None,
         only_expose_public_allowed_properties: Optional[bool] = True,
     ):
+        """
+        Dispatch context used in the builder.
+
+        :param request: The HTTP request from the view.
+        :param page: The page related to the dispatch.
+        :param workflow_action: The workflow action being executed, if any.
+        :param element: An optional element that triggered the dispatch.
+        :param offset: When we dispatch a list service, starts by that offset.
+        :param count: When we dispatch a list service returns that max amount of record.
+        :param record_id: If we want to narrow down the results of a list service to
+          only the record with this Id.
+        :param only_expose_public_allowed_properties: Determines whether only public
+            allowed properties should be exposed. Defaults to True.
+        """
+
         self.request = request
         self.page = page
         self.workflow_action = workflow_action
         self.element = element
+        self.only_record_id = only_record_id
 
         # Overrides the `request` GET offset/count values.
         self.offset = offset
diff --git a/backend/src/baserow/contrib/builder/elements/mixins.py b/backend/src/baserow/contrib/builder/elements/mixins.py
index 1b8f6856c..2bc98ca0c 100644
--- a/backend/src/baserow/contrib/builder/elements/mixins.py
+++ b/backend/src/baserow/contrib/builder/elements/mixins.py
@@ -517,9 +517,17 @@ class CollectionElementTypeMixin:
             .items()
             if any(options.values())
         ]
+
         if data_source and property_options:
             properties.setdefault(data_source.service_id, []).extend(property_options)
 
+        # We need the id for the element
+        if data_source and data_source.service_id:
+            service = data_source.service.specific
+            id_property = service.get_type().get_id_property(service)
+            if id_property not in properties.setdefault(service.id, []):
+                properties[service.id].append(id_property)
+
         return properties
 
 
diff --git a/backend/src/baserow/contrib/integrations/local_baserow/service_types.py b/backend/src/baserow/contrib/integrations/local_baserow/service_types.py
index 55efbd031..a3ca3fe34 100644
--- a/backend/src/baserow/contrib/integrations/local_baserow/service_types.py
+++ b/backend/src/baserow/contrib/integrations/local_baserow/service_types.py
@@ -1082,6 +1082,9 @@ class LocalBaserowListRowsUserServiceType(
             # Ensure that only used fields are fetched from the database.
             queryset = queryset.only(*available_fields.intersection(only_field_names))
 
+        if dispatch_context.only_record_id is not None:
+            queryset = queryset.filter(id=dispatch_context.only_record_id)
+
         offset, count = dispatch_context.range(service)
 
         # We query one more row to be able to know if there is another page that can be
diff --git a/backend/src/baserow/core/services/dispatch_context.py b/backend/src/baserow/core/services/dispatch_context.py
index 92b675969..99181cbc8 100644
--- a/backend/src/baserow/core/services/dispatch_context.py
+++ b/backend/src/baserow/core/services/dispatch_context.py
@@ -10,6 +10,13 @@ from baserow.core.services.utils import ServiceAdhocRefinements
 class DispatchContext(RuntimeFormulaContext, ABC):
     own_properties = []
 
+    """
+    Should return the record id requested for the given service. Used by list
+    services to select only one record. For instance by the builder current record
+    data provider to narrow down the result of a list service.
+    """
+    only_record_id = None
+
     def __init__(self):
         self.cache = {}  # can be used by data providers to save queries
         super().__init__()
diff --git a/backend/src/baserow/core/services/registries.py b/backend/src/baserow/core/services/registries.py
index 1dd48d867..706337fcb 100644
--- a/backend/src/baserow/core/services/registries.py
+++ b/backend/src/baserow/core/services/registries.py
@@ -68,6 +68,29 @@ class ServiceType(
     # `DISPATCH_WORKFLOW_ACTION` should be chosen.
     dispatch_type = None
 
+    def get_id_property(self, service: Service) -> str:
+        """
+        Returns the property name that contains the unique `ID` of a row for this
+        service.
+
+        :param service: the instance of the service.
+        :return: a string identifying the ID property name.
+        """
+
+        # Sane default
+        return "id"
+
+    def get_name_property(self, service: Service) -> Optional[str]:
+        """
+        We need the name of the records for some elements (like the record selector).
+        This method returns it depending on the service.
+
+        :param service: the instance of the service.
+        :return: a string identifying the name property name.
+        """
+
+        return None
+
     def prepare_values(
         self,
         values: Dict[str, Any],
@@ -343,29 +366,6 @@ class ListServiceTypeMixin:
 
     returns_list = True
 
-    def get_id_property(self, service: Service) -> str:
-        """
-        Returns the property name that contains the unique `ID` of a row for this
-        service.
-
-        :param service: the instance of the service.
-        :return: a string identifying the ID property name.
-        """
-
-        # Sane default
-        return "id"
-
-    def get_name_property(self, service: Service) -> Optional[str]:
-        """
-        We need the name of the records for some elements (like the record selector).
-        This method returns it depending on the service.
-
-        :param service: the instance of the service.
-        :return: a string identifying the name property name.
-        """
-
-        return None
-
     @abstractmethod
     def get_record_names(
         self,
diff --git a/backend/tests/baserow/contrib/builder/api/data_sources/test_data_source_views.py b/backend/tests/baserow/contrib/builder/api/data_sources/test_data_source_views.py
index 502fb8cf7..f82ebac6a 100644
--- a/backend/tests/baserow/contrib/builder/api/data_sources/test_data_source_views.py
+++ b/backend/tests/baserow/contrib/builder/api/data_sources/test_data_source_views.py
@@ -1961,6 +1961,7 @@ def test_dispatch_data_sources_list_rows_with_elements(
                 # Although this Data Source has 2 Fields/Columns, only one is
                 # returned since only one field_id is used by the Table.
                 f"field_{field_id}": getattr(row, f"field_{field_id}"),
+                "id": row.id,
             }
         )
 
@@ -2043,6 +2044,7 @@ def test_dispatch_data_sources_get_row_with_elements(
     assert response.json() == {
         str(data_source.id): {
             f"field_{field_id}": getattr(rows[db_row_id], f"field_{field_id}"),
+            "id": rows[db_row_id].id,
         }
     }
 
@@ -2148,6 +2150,7 @@ def test_dispatch_data_sources_get_and_list_rows_with_elements(
     assert response.json() == {
         str(data_source_1.id): {
             f"field_{fields_1[0].id}": getattr(rows_1[0], f"field_{fields_1[0].id}"),
+            "id": rows_1[0].id,
         },
         # Although this Data Source has 2 Fields/Columns, only one is returned
         # since only one field_id is used by the Table.
@@ -2158,6 +2161,7 @@ def test_dispatch_data_sources_get_and_list_rows_with_elements(
                     f"field_{fields_2[0].id}": getattr(
                         rows_2[0], f"field_{fields_2[0].id}"
                     ),
+                    "id": rows_2[0].id,
                 },
             ],
         },
diff --git a/backend/tests/baserow/contrib/builder/api/data_sources/test_public_domain_views.py b/backend/tests/baserow/contrib/builder/api/data_sources/test_public_domain_views.py
index e2d0b3799..9fa768cec 100644
--- a/backend/tests/baserow/contrib/builder/api/data_sources/test_public_domain_views.py
+++ b/backend/tests/baserow/contrib/builder/api/data_sources/test_public_domain_views.py
@@ -251,9 +251,7 @@ def test_dispatch_data_sources_list_rows_with_elements(
     )
 
     expected_results = [
-        {
-            f"field_{field_id}": getattr(row, f"field_{field_id}"),
-        }
+        {f"field_{field_id}": getattr(row, f"field_{field_id}"), "id": row.id}
         for row in data_source_fixture["rows"]
     ]
 
@@ -332,6 +330,7 @@ def test_dispatch_data_sources_get_row_with_elements(
     assert response.json() == {
         str(data_source.id): {
             f"field_{field_id}": getattr(rows[db_row_id], f"field_{field_id}"),
+            "id": rows[db_row_id].id,
         }
     }
 
@@ -431,6 +430,7 @@ def test_dispatch_data_sources_get_and_list_rows_with_elements(
     assert response.json() == {
         str(data_source_1.id): {
             f"field_{fields_1[0].id}": getattr(rows_1[0], f"field_{fields_1[0].id}"),
+            "id": rows_1[0].id,
         },
         # Although this Data Source has 2 Fields/Columns, only one is returned
         # since only one field_id is used by the Table.
@@ -441,6 +441,7 @@ def test_dispatch_data_sources_get_and_list_rows_with_elements(
                     f"field_{fields_2[0].id}": getattr(
                         rows_2[0], f"field_{fields_2[0].id}"
                     ),
+                    "id": rows_2[0].id,
                 },
             ],
         },
@@ -537,7 +538,9 @@ def test_dispatch_data_sources_list_rows_with_elements_and_role(
             # Field should only be visible if the user's role allows them
             # to see the data source fields.
 
-            expected_results.append({field_name: getattr(row, field_name)})
+            expected_results.append(
+                {field_name: getattr(row, field_name), "id": row.id}
+            )
         else:
             expected_results.append({})
 
diff --git a/backend/tests/baserow/contrib/builder/api/domains/test_domain_public_views.py b/backend/tests/baserow/contrib/builder/api/domains/test_domain_public_views.py
index 83f253773..d5a008cd8 100644
--- a/backend/tests/baserow/contrib/builder/api/domains/test_domain_public_views.py
+++ b/backend/tests/baserow/contrib/builder/api/domains/test_domain_public_views.py
@@ -857,10 +857,12 @@ def test_public_dispatch_data_source_view_returns_all_fields(
         "has_next_page": False,
         "results": [
             {
+                "id": rows[0].id,
                 f"field_{fields[0].id}": "Paneer Tikka",
                 f"field_{fields[1].id}": "5",
             },
             {
+                "id": rows[1].id,
                 f"field_{fields[0].id}": "Gobi Manchurian",
                 f"field_{fields[1].id}": "8",
             },
@@ -1134,7 +1136,7 @@ def test_public_dispatch_data_sources_list_rows_with_elements_and_role(
 
     expected_results = []
     for row in data_source_element_roles_fixture["rows"]:
-        result = {}
+        result = {"id": row.id}
         if expect_fields:
             # Field should only be visible if the user's role allows them
             # to see the data source fields.
@@ -1318,15 +1320,17 @@ def test_public_dispatch_data_sources_list_rows_with_page_visibility_all(
 
     assert response.status_code == HTTP_200_OK
 
+    rows = data_source_element_roles_fixture["rows"]
+
     if expect_fields:
         field_name = f"field_{field_id}"
         assert response.json() == {
             str(data_source.id): {
                 "has_next_page": False,
                 "results": [
-                    {field_name: "Apple"},
-                    {field_name: "Banana"},
-                    {field_name: "Cherry"},
+                    {field_name: "Apple", "id": rows[0].id},
+                    {field_name: "Banana", "id": rows[1].id},
+                    {field_name: "Cherry", "id": rows[2].id},
                 ],
             },
         }
@@ -1632,15 +1636,17 @@ def test_public_dispatch_data_sources_list_rows_with_page_visibility_logged_in(
 
     assert response.status_code == HTTP_200_OK
 
+    rows = data_source_element_roles_fixture["rows"]
+
     if expect_fields:
         field_name = f"field_{field_id}"
         assert response.json() == {
             str(data_source.id): {
                 "has_next_page": False,
                 "results": [
-                    {field_name: "Apple"},
-                    {field_name: "Banana"},
-                    {field_name: "Cherry"},
+                    {field_name: "Apple", "id": rows[0].id},
+                    {field_name: "Banana", "id": rows[1].id},
+                    {field_name: "Cherry", "id": rows[2].id},
                 ],
             },
         }
diff --git a/backend/tests/baserow/contrib/builder/api/workflow_actions/test_workflow_actions_views.py b/backend/tests/baserow/contrib/builder/api/workflow_actions/test_workflow_actions_views.py
index 360677696..862e78110 100644
--- a/backend/tests/baserow/contrib/builder/api/workflow_actions/test_workflow_actions_views.py
+++ b/backend/tests/baserow/contrib/builder/api/workflow_actions/test_workflow_actions_views.py
@@ -1,4 +1,3 @@
-import json
 from unittest.mock import patch
 
 from django.db import transaction
@@ -677,7 +676,7 @@ def test_dispatch_local_baserow_upsert_row_workflow_action_with_current_record(
         }
         response = api_client.post(
             url,
-            {"current_record": 123},
+            {"current_record": {"index": 123, "record_id": 123}},
             format="json",
             HTTP_AUTHORIZATION=f"JWT {token}",
         )
@@ -689,7 +688,7 @@ def test_dispatch_local_baserow_upsert_row_workflow_action_with_current_record(
 
 
 @pytest.mark.django_db(transaction=True)
-def test_dispatch_local_baserow_upsert_row_workflow_action_with_adhoc_refinements(
+def test_dispatch_local_baserow_upsert_row_workflow_action_with_unmatching_index_and_record_id(
     api_client, data_fixture
 ):
     with transaction.atomic():
@@ -705,7 +704,7 @@ def test_dispatch_local_baserow_upsert_row_workflow_action_with_adhoc_refinement
             ],
         )
         field = table.field_set.get()
-        RowHandler().create_rows(
+        rows = RowHandler().create_rows(
             user,
             table,
             rows_values=[
@@ -766,17 +765,6 @@ def test_dispatch_local_baserow_upsert_row_workflow_action_with_adhoc_refinement
         kwargs={"workflow_action_id": workflow_action.id},
     )
 
-    advanced_filters = {
-        "filter_type": "OR",
-        "filters": [
-            {
-                "field": field.id,
-                "type": "contains",
-                "value": "construction",
-            }
-        ],
-    }
-
     with patch(
         "baserow.contrib.builder.handler.get_builder_used_property_names"
     ) as used_properties_mock:
@@ -786,36 +774,34 @@ def test_dispatch_local_baserow_upsert_row_workflow_action_with_adhoc_refinement
         }
         model = table.get_model()
 
-        # 1. The filters reduce it to 3 results.
-        # 2. The search query reduces it to 2 results.
-        # 3. We sort alphabetically, and dispatch the first one,
-        #   "Complex Construction Design".
-        url_with_querystring = (
-            f"{url}?filters={json.dumps(advanced_filters)}"
-            f"&search_query=design&order_by={field.db_column}"
-        )
-
-        # Dispatch at index=0, this will be "Complex Construction Design".
+        # Dispatch at index=0 but row 3 id, this will be "Complex Construction Design".
         response = api_client.post(
-            url_with_querystring,
-            {"current_record": 0, "data_source": {"element": table_element.id}},
+            url,
+            {
+                "current_record": {"index": 0, "record_id": rows[2].id},
+                "data_source": {"element": table_element.id},
+            },
             format="json",
             HTTP_AUTHORIZATION=f"JWT {token}",
         )
         assert response.status_code == HTTP_200_OK
-        row3 = model.objects.get(pk=3)
-        assert getattr(row3, f"field_{field.id}") == "Updated row 3"
+        row3 = model.objects.get(pk=rows[2].id)
+        assert getattr(row3, f"field_{field.id}") == f"Updated row {rows[2].id}"
 
-        # Dispatch at index=0, this will now be "Simple Construction Design".
+        # Dispatch at index=0 but row 4 id,
+        # this will now be "Simple Construction Design".
         response = api_client.post(
-            url_with_querystring,
-            {"current_record": 0, "data_source": {"element": table_element.id}},
+            url,
+            {
+                "current_record": {"index": 0, "record_id": rows[3].id},
+                "data_source": {"element": table_element.id},
+            },
             format="json",
             HTTP_AUTHORIZATION=f"JWT {token}",
         )
         assert response.status_code == HTTP_200_OK
-        row4 = model.objects.get(pk=4)
-        assert getattr(row4, f"field_{field.id}") == "Updated row 4"
+        row4 = model.objects.get(pk=rows[3].id)
+        assert getattr(row4, f"field_{field.id}") == f"Updated row {rows[3].id}"
 
 
 @pytest.mark.django_db
diff --git a/backend/tests/baserow/contrib/builder/data_providers/test_data_provider_types.py b/backend/tests/baserow/contrib/builder/data_providers/test_data_provider_types.py
index c09163296..504bdb915 100644
--- a/backend/tests/baserow/contrib/builder/data_providers/test_data_provider_types.py
+++ b/backend/tests/baserow/contrib/builder/data_providers/test_data_provider_types.py
@@ -1208,21 +1208,15 @@ def test_current_record_provider_get_data_chunk_without_record_index(data_fixtur
 def test_current_record_provider_get_data_chunk_for_idx():
     current_record_provider = CurrentRecordDataProviderType()
     fake_request = HttpRequest()
-    fake_request.data = {"current_record": 123}
+    fake_request.data = {"current_record": {"index": 123, "record_id": 123}}
     dispatch_context = BuilderDispatchContext(fake_request, None)
     assert current_record_provider.get_data_chunk(dispatch_context, ["__idx__"]) == 123
 
 
 @pytest.mark.django_db
 def test_current_record_provider_get_data_chunk(data_fixture):
-    current_record_provider = CurrentRecordDataProviderType()
-
     user, token = data_fixture.create_user_and_token()
 
-    fake_request = HttpRequest()
-    fake_request.user = user
-    fake_request.data = {"current_record": 0}
-
     table, fields, rows = data_fixture.build_table(
         user=user,
         columns=[
@@ -1252,10 +1246,16 @@ def test_current_record_provider_get_data_chunk(data_fixture):
         page=page, element=button_element, event=EventTypes.CLICK, user=user
     )
 
+    fake_request = HttpRequest()
+    fake_request.user = user
+    fake_request.data = {"current_record": {"index": 0, "record_id": rows[0].id}}
+
     dispatch_context = BuilderDispatchContext(
         fake_request, page, workflow_action, only_expose_public_allowed_properties=False
     )
 
+    current_record_provider = CurrentRecordDataProviderType()
+
     assert (
         current_record_provider.get_data_chunk(dispatch_context, [field.db_column])
         == "Badger"
diff --git a/backend/tests/baserow/contrib/builder/elements/test_repeat_element_type.py b/backend/tests/baserow/contrib/builder/elements/test_repeat_element_type.py
index ef97d03b0..8d8099130 100644
--- a/backend/tests/baserow/contrib/builder/elements/test_repeat_element_type.py
+++ b/backend/tests/baserow/contrib/builder/elements/test_repeat_element_type.py
@@ -208,7 +208,7 @@ def test_extract_properties_includes_schema_property_for_nested_collection(
     )
 
     properties = RepeatElementType().extract_properties(parent_repeat)
-    assert properties == {}
+    assert properties == {data_source.service_id: ["id"]}
 
     # Create a child Repeat with a schema_property
     child_repeat = data_fixture.create_builder_repeat_element(
@@ -223,8 +223,10 @@ def test_extract_properties_includes_schema_property_for_nested_collection(
 
     properties = RepeatElementType().extract_properties(child_repeat, **formula_context)
 
-    # We expect that the schema_property field ID to be present
-    assert properties == {data_source.service_id: [f"field_{multiple_select_field.id}"]}
+    # We expect that the schema_property field to be present and the ID
+    assert properties == {
+        data_source.service_id: [f"field_{multiple_select_field.id}", "id"]
+    }
 
 
 @pytest.mark.django_db
@@ -283,4 +285,6 @@ def test_extract_properties_includes_schema_property_for_single_row(
     )
 
     properties = RepeatElementType().extract_properties(repeat)
-    assert properties == {data_source.service_id: [f"field_{multiple_select_field.id}"]}
+    assert properties == {
+        data_source.service_id: [f"field_{multiple_select_field.id}", "id"]
+    }
diff --git a/backend/tests/baserow/contrib/builder/test_formula_property_extractor.py b/backend/tests/baserow/contrib/builder/test_formula_property_extractor.py
index ef24d290a..207d5bded 100644
--- a/backend/tests/baserow/contrib/builder/test_formula_property_extractor.py
+++ b/backend/tests/baserow/contrib/builder/test_formula_property_extractor.py
@@ -110,10 +110,10 @@ def test_get_builder_used_property_names_returns_all_property_names(data_fixture
 
     assert list(results) == unordered(["all", "external", "internal"])
     assert results["all"][data_source.service_id] == unordered(
-        [f"field_{field.id}" for field in fields]
+        [f"field_{field.id}" for field in fields] + ["id"]
     )
     assert results["external"][data_source.service_id] == unordered(
-        [f"field_{field.id}" for field in fields]
+        [f"field_{field.id}" for field in fields] + ["id"]
     )
     assert results["internal"] == {}
 
@@ -169,10 +169,10 @@ def test_get_builder_used_property_names_returns_some_property_names(data_fixtur
     # only one property, ensure that specific property is the only one returned.
     assert results == {
         "all": {
-            data_source.service_id: [f"field_{fields[0].id}"],
+            data_source.service_id: [f"field_{fields[0].id}", "id"],
         },
         "external": {
-            data_source.service_id: [f"field_{fields[0].id}"],
+            data_source.service_id: [f"field_{fields[0].id}", "id"],
         },
         "internal": {},
     }
@@ -997,6 +997,7 @@ def test_get_builder_used_property_names_returns_merged_property_names_integrati
         "all": {
             data_source.service_id: sorted(
                 [
+                    "id",
                     f"field_{fields[0].id}",
                     f"field_{fields[2].id}",
                 ]
@@ -1019,6 +1020,7 @@ def test_get_builder_used_property_names_returns_merged_property_names_integrati
         "external": {
             data_source.service_id: [
                 f"field_{fields[0].id}",  # From heading_element_1
+                "id",
             ],
             data_source_2.service_id: [
                 f"field_{fields[2].id}"
diff --git a/web-frontend/modules/builder/components/elements/baseComponents/ABTable.vue b/web-frontend/modules/builder/components/elements/baseComponents/ABTable.vue
index 2b657c740..fdc54e5db 100644
--- a/web-frontend/modules/builder/components/elements/baseComponents/ABTable.vue
+++ b/web-frontend/modules/builder/components/elements/baseComponents/ABTable.vue
@@ -10,12 +10,13 @@
         <slot name="field-name" :field="field">{{ field.name }}</slot>
       </th>
     </template>
-    <template #cell-content="{ rowIndex, value, field }">
+    <template #cell-content="{ rowIndex, value, field, row }">
       <slot
         name="cell-content"
         :value="value"
         :field="field"
         :row-index="rowIndex"
+        :row="row"
       >
         <td :key="field.id" class="ab-table__cell">
           <div class="ab-table__cell-content">
diff --git a/web-frontend/modules/builder/components/elements/components/BaserowTable.vue b/web-frontend/modules/builder/components/elements/components/BaserowTable.vue
index 8f70061a3..4d57ad968 100644
--- a/web-frontend/modules/builder/components/elements/components/BaserowTable.vue
+++ b/web-frontend/modules/builder/components/elements/components/BaserowTable.vue
@@ -25,6 +25,7 @@
                 :value="row[field.name]"
                 :field="field"
                 :row-index="index"
+                :row="row"
               >
                 <td :key="field.id" class="baserow-table__cell">
                   {{ row[field.name] }}
@@ -55,6 +56,7 @@
                 :value="row[field.name]"
                 :field="field"
                 :row-index="rowIndex"
+                :row="row"
               >
                 <td
                   class="baserow-table__cell"
diff --git a/web-frontend/modules/builder/components/elements/components/RepeatElement.vue b/web-frontend/modules/builder/components/elements/components/RepeatElement.vue
index 93d0481dd..4f8e4cce7 100644
--- a/web-frontend/modules/builder/components/elements/components/RepeatElement.vue
+++ b/web-frontend/modules/builder/components/elements/components/RepeatElement.vue
@@ -28,12 +28,13 @@
                   v-if="index === 0 && isEditMode"
                   :key="`${child.id}-${index}`"
                   :element="child"
-                  :application-context-additions="{
-                    recordIndexPath: [
-                      ...applicationContext.recordIndexPath,
-                      index,
-                    ],
-                  }"
+                  :application-context-additions="
+                    getPerRecordApplicationContextAddition({
+                      applicationContext,
+                      row: content,
+                      rowIndex: index,
+                    })
+                  "
                   @move="$emit('move', $event)"
                 />
                 <!-- Other iterations are not editable -->
@@ -44,12 +45,13 @@
                   :key="`${child.id}_${index}`"
                   :element="child"
                   :force-mode="isEditMode ? 'public' : mode"
-                  :application-context-additions="{
-                    recordIndexPath: [
-                      ...applicationContext.recordIndexPath,
-                      index,
-                    ],
-                  }"
+                  :application-context-additions="
+                    getPerRecordApplicationContextAddition({
+                      applicationContext,
+                      row: content,
+                      rowIndex: index,
+                    })
+                  "
                   :class="{
                     'repeat-element__preview': index > 0 && isEditMode,
                   }"
diff --git a/web-frontend/modules/builder/components/elements/components/TableElement.vue b/web-frontend/modules/builder/components/elements/components/TableElement.vue
index 1c43522e1..89279de4a 100644
--- a/web-frontend/modules/builder/components/elements/components/TableElement.vue
+++ b/web-frontend/modules/builder/components/elements/components/TableElement.vue
@@ -13,7 +13,7 @@
       :style="getStyleOverride('table')"
       :orientation="orientation"
     >
-      <template #cell-content="{ rowIndex, field, value }">
+      <template #cell-content="{ rowIndex, field, value, row }">
         <!--
         -- We force-self-alignment to `auto` here to prevent some self-positioning
         -- like in buttons or links. we want to position the content through the table
@@ -34,14 +34,14 @@
               :is="collectionFieldTypes[field.type].component"
               :element="element"
               :field="field"
-              :application-context-additions="{
-                recordIndexPath: [
-                  ...applicationContext.recordIndexPath,
+              :application-context-additions="
+                getPerRecordApplicationContextAddition({
+                  applicationContext,
+                  row,
                   rowIndex,
-                ],
-                field,
-                dispatchRefinements: adhocRefinements,
-              }"
+                  field,
+                })
+              "
               v-bind="value"
             />
           </div>
@@ -117,6 +117,7 @@ export default {
           })
         )
         newRow.__id__ = uuid()
+        newRow.__recordId__ = row.__recordId__
         return newRow
       })
     },
diff --git a/web-frontend/modules/builder/dataProviderTypes.js b/web-frontend/modules/builder/dataProviderTypes.js
index 2ec8a58d0..6840709f8 100644
--- a/web-frontend/modules/builder/dataProviderTypes.js
+++ b/web-frontend/modules/builder/dataProviderTypes.js
@@ -384,7 +384,10 @@ export class CurrentRecordDataProviderType extends DataProviderType {
   }
 
   getActionDispatchContext(applicationContext) {
-    return applicationContext.recordIndexPath.at(-1)
+    return {
+      record_id: applicationContext.recordId,
+      index: applicationContext.recordIndexPath.at(-1),
+    }
   }
 
   getDataChunk(applicationContext, path) {
diff --git a/web-frontend/modules/builder/eventTypes.js b/web-frontend/modules/builder/eventTypes.js
index 7c6f63971..09d871366 100644
--- a/web-frontend/modules/builder/eventTypes.js
+++ b/web-frontend/modules/builder/eventTypes.js
@@ -64,19 +64,6 @@ export class Event {
         )
       }
 
-      // If we're firing a workflow action, and the collection element it's associated
-      // with is currently being filtered, we must forward this on to the workflow
-      // action dispatch so that the backend fires at the correct current_record.
-      if (
-        Object.prototype.hasOwnProperty.call(
-          applicationContext,
-          'dispatchRefinements'
-        )
-      ) {
-        workflowActionContext.dispatchRefinements =
-          applicationContext.dispatchRefinements
-      }
-
       const localResolveFormula = (formula) => {
         const formulaFunctions = {
           get: (name) => {
diff --git a/web-frontend/modules/builder/mixins/collectionElement.js b/web-frontend/modules/builder/mixins/collectionElement.js
index c70be12f9..28e5b0acb 100644
--- a/web-frontend/modules/builder/mixins/collectionElement.js
+++ b/web-frontend/modules/builder/mixins/collectionElement.js
@@ -176,5 +176,22 @@ export default {
         this.contentFetchEnabled = false
       }
     },
+    getPerRecordApplicationContextAddition({
+      applicationContext,
+      row,
+      rowIndex,
+      field = null,
+    }) {
+      const newApplicationContext = {
+        recordIndexPath: [...applicationContext.recordIndexPath, rowIndex],
+      }
+      if (field) {
+        newApplicationContext.field = field
+      }
+      if (this.element.data_source_id) {
+        newApplicationContext.recordId = row.__recordId__
+      }
+      return newApplicationContext
+    },
   },
 }
diff --git a/web-frontend/modules/builder/services/workflowAction.js b/web-frontend/modules/builder/services/workflowAction.js
index d891fd143..ae7930b80 100644
--- a/web-frontend/modules/builder/services/workflowAction.js
+++ b/web-frontend/modules/builder/services/workflowAction.js
@@ -1,5 +1,3 @@
-import { prepareDispatchParams } from '@baserow/modules/builder/utils/params'
-
 export default (client) => {
   return {
     create(pageId, workflowActionType, eventType, configuration = null) {
@@ -35,12 +33,10 @@ export default (client) => {
         payload
       )
     },
-    dispatch(workflowActionId, data, dispatchRefinements) {
-      const params = prepareDispatchParams(dispatchRefinements)
+    dispatch(workflowActionId, data) {
       return client.post(
         `builder/workflow_action/${workflowActionId}/dispatch/`,
-        data,
-        { params }
+        data
       )
     },
   }
diff --git a/web-frontend/modules/builder/store/elementContent.js b/web-frontend/modules/builder/store/elementContent.js
index dc98fcc13..68739d5ea 100644
--- a/web-frontend/modules/builder/store/elementContent.js
+++ b/web-frontend/modules/builder/store/elementContent.js
@@ -246,7 +246,10 @@ const actions = {
           // using the results key and set the range for future paging.
           commit('SET_CONTENT', {
             element,
-            value: data.results,
+            value: data.results.map((row) => ({
+              ...row,
+              __recordId__: row[serviceType.getIdProperty(service, row)],
+            })),
             range,
           })
         } else {
diff --git a/web-frontend/modules/builder/store/workflowAction.js b/web-frontend/modules/builder/store/workflowAction.js
index 39fb18762..844214804 100644
--- a/web-frontend/modules/builder/store/workflowAction.js
+++ b/web-frontend/modules/builder/store/workflowAction.js
@@ -200,15 +200,10 @@ const actions = {
       updateContext.promiseResolve = resolve
     })
   },
-  async dispatchAction(
-    { dispatch },
-    { workflowActionId, workflowActionContext, data }
-  ) {
-    const { dispatchRefinements = {} } = workflowActionContext
+  async dispatchAction({ dispatch }, { workflowActionId, data }) {
     const { data: result } = await WorkflowActionService(this.$client).dispatch(
       workflowActionId,
-      data,
-      dispatchRefinements
+      data
     )
     return result
   },
diff --git a/web-frontend/modules/builder/workflowActionTypes.js b/web-frontend/modules/builder/workflowActionTypes.js
index 00e76001e..74b86f7d8 100644
--- a/web-frontend/modules/builder/workflowActionTypes.js
+++ b/web-frontend/modules/builder/workflowActionTypes.js
@@ -180,10 +180,8 @@ export class RefreshDataSourceWorkflowActionType extends WorkflowActionType {
 
 export class WorkflowActionServiceType extends WorkflowActionType {
   execute({ workflowAction: { id }, applicationContext, resolveFormula }) {
-    const { workflowActionContext } = applicationContext
     return this.app.store.dispatch('workflowAction/dispatchAction', {
       workflowActionId: id,
-      workflowActionContext,
       data: DataProviderType.getAllActionDispatchContext(
         this.app.$registry.getAll('builderDataProvider'),
         applicationContext
diff --git a/web-frontend/modules/core/serviceTypes.js b/web-frontend/modules/core/serviceTypes.js
index 773765080..6b65ccca2 100644
--- a/web-frontend/modules/core/serviceTypes.js
+++ b/web-frontend/modules/core/serviceTypes.js
@@ -35,6 +35,22 @@ export class ServiceType extends Registerable {
     return false
   }
 
+  /**
+   * In a service which returns a list, this method is used to
+   * return the name of the given record.
+   */
+  getRecordName(service, record) {
+    throw new Error('Must be set on the type.')
+  }
+
+  /**
+   * In a service which returns a list, this method is used to
+   * return the id of the given record.
+   */
+  getIdProperty(service, record) {
+    throw new Error('Must be set on the type.')
+  }
+
   /**
    * The maximum number of records that can be returned by this service
    */
diff --git a/web-frontend/modules/integrations/serviceTypes.js b/web-frontend/modules/integrations/serviceTypes.js
index 64d0f5074..ba72332b4 100644
--- a/web-frontend/modules/integrations/serviceTypes.js
+++ b/web-frontend/modules/integrations/serviceTypes.js
@@ -38,12 +38,8 @@ export class LocalBaserowTableServiceType extends ServiceType {
     return service.context_data_schema
   }
 
-  /**
-   * In a Local Baserow service which returns a list, this method is used to
-   * return the name of the given record.
-   */
-  getRecordName(service, record) {
-    return ''
+  getIdProperty(service, record) {
+    return 'id'
   }
 
   /**