Skip to content

Conversation

@pemontto
Copy link
Contributor

@pemontto pemontto commented Nov 19, 2025

Summary

Group By and Aggregate go together like peas and carrots 🥕 - Forrest Gump (probably)

This PR adds Group By support to the Aggregate node, enabling users to group items by field values before aggregating. This is a fundamental data manipulation pattern that brings n8n closer to standards like SQL GROUP BY, Pandas groupby(), or Excel Pivot Tables.

Features

✅ Group by fields: Split data into groups based on one or multiple fields (comma-separated).
✅ Flexible Modes: Supports both "Individual Fields" and "All Item Data" aggregation modes within the groups.
✅ Binary Support: Full support for including/grouping binary data.

Why Add This?

Group By is essential for structured data aggregation. It allows users to move from a global aggregation to specific buckets.

Before (One aggregation across all items):

After (Group By 'domain'):

// Output: 3 items
[
  { domain: "yahoo.com", emails: ["[email protected]", "[email protected]"] },
  { domain: "gmail.com", emails: ["[email protected]"] },
  { domain: "hotmail.com", emails: ["[email protected]"] }
]

Outstanding Questions

Item Linking (pairedItem): Initially, I mapped all constituent input items to the pairedItem array for the resulting group (which seemed semantically correct). However, I didn't observe any functional use in the UI when compared to just mapping the first item. I found just using the first item at least gives me a way to trace back to an originating item, i.e. allowing $('previous node').item to work.

Mapping all related items as an array would return the same as if no mapping took place:
image

Sample Workflow

image
Click to expand workflow JSON
{
  "nodes": [
    {
      "parameters": {},
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        -80
      ],
      "id": "28bdc599-680f-4306-b43f-a972c5ac5ab4",
      "name": "When clicking ‘Execute workflow’"
    },
    {
      "parameters": {
        "aggregate": "aggregateAllItemData",
        "options": {
          "groupByFields": "domain",
          "includeBinaries": true
        }
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        880,
        -176
      ],
      "id": "29086978-669e-49de-a153-8ab5cf423f3a",
      "name": "Aggregate All"
    },
    {
      "parameters": {
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "email"
            },
            {
              "fieldToAggregate": "confirmed",
              "renameField": true,
              "outputFieldName": "T/F"
            }
          ]
        },
        "options": {
          "groupByFields": "domain"
        }
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        448,
        16
      ],
      "id": "68650cbd-3885-4e68-9398-57ac13b75d55",
      "name": "Aggregate Individual"
    },
    {
      "parameters": {
        "operation": "toText",
        "sourceProperty": "email",
        "options": {}
      },
      "type": "n8n-nodes-base.convertToFile",
      "typeVersion": 1.1,
      "position": [
        448,
        -176
      ],
      "id": "4facc738-c1ef-4923-a44b-24e687a98adf",
      "name": "Convert to File"
    },
    {
      "parameters": {
        "mode": "raw",
        "jsonOutput": "={{ \n$('Emails').item.json\n}}",
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        656,
        -176
      ],
      "id": "c2ff68b1-7552-444a-93b5-dbe6b5f903a3",
      "name": "Add Fields"
    },
    {
      "parameters": {
        "jsCode": "return [\n  {\n    json: {\n      email: \"[email protected]\",\n      confirmed: true,\n      domain: 'hotmail.com'\n    }\n  },\n  {\n    json: {\n      email: \"[email protected]\",\n      confirmed: false,\n      domain: 'yahoo.com'\n    }\n  },\n  {\n    json: {\n      email: \"[email protected]\",\n      confirmed: false,\n      domain: 'yahoo.com'\n    }\n  },\n  {\n    json: {\n      email: \"[email protected]\",\n      confirmed: false,\n      domain: 'gmail.com'\n    }\n  },\n  {\n    json: {\n      email: \"[email protected]\",\n      confirmed: true,\n      domain: 'yahoo.com'\n    }\n  },\n]"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        224,
        -80
      ],
      "id": "c74ca043-1115-4d60-9834-f08cab7dfd04",
      "name": "Emails"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "f5422421-b865-4488-b261-33a95f51b724",
              "name": "orig_data",
              "value": "={{ $('Emails').item.json }}",
              "type": "string"
            }
          ]
        },
        "includeOtherFields": true,
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        1088,
        -176
      ],
      "id": "12963bce-e1c4-42f3-b86b-ca3e3af8ce7f",
      "name": "PairedItem"
    }
  ],
  "connections": {
    "When clicking ‘Execute workflow’": {
      "main": [
        [
          {
            "node": "Emails",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate All": {
      "main": [
        [
          {
            "node": "PairedItem",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert to File": {
      "main": [
        [
          {
            "node": "Add Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Fields": {
      "main": [
        [
          {
            "node": "Aggregate All",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Emails": {
      "main": [
        [
          {
            "node": "Aggregate Individual",
            "type": "main",
            "index": 0
          },
          {
            "node": "Convert to File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {}
}

Review / Merge checklist

  • PR title and summary are descriptive. (conventions)
    -->
  • Docs updated or follow-up ticket created.
  • Tests included.
  • PR Labeled with release/backport (if the PR is an urgent fix that needs to be backported)

Note

Adds configurable group-by to Aggregate with nested-field conflict checks, per-group aggregation for both modes, and binary handling, plus comprehensive tests and example workflow.

  • Aggregate node (packages/nodes-base/nodes/Transform/Aggregate/Aggregate.node.ts):
    • Feature: New options.groupByFields enabling grouping by one or more fields (dot-notation supported) with per-group outputs.
    • Aggregation: Supports grouping for both aggregateIndividualFields and aggregateAllItemData, re-aggregating within each group.
    • Validation:
      • Reject non-scalar group-by values.
      • Detect nested field path conflicts via hasNestedPathConflict between group-by fields and output/destination fields.
      • Enforce unique output field names; preserve existing missing/merge behaviors.
    • Binaries: Include binaries per group with optional uniqueness filtering.
    • Minor: options placeholder changed to Add Option.
  • Tests (packages/nodes-base/nodes/Transform/Aggregate/test/Aggregate.groupby.test.ts):
    • Cover conflicts (parent/child paths), duplicate outputs, empty fields, non-scalar grouping, success cases, and binary inclusion.
  • Example workflow (packages/nodes-base/nodes/Transform/Aggregate/test/workflow.groupby.json):
    • Demonstrates grouping by domain for both aggregation modes and field include/exclude variants.

Written by Cursor Bugbot for commit a93b338. This will update automatically on new commits. Configure here.

Introduced the ability to group items based on specified fields.
Added validation for conflicts between group-by fields and aggregated/destination fields.
Included comprehensive unit tests and a workflow for the new grouping feature.
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR is being reviewed by Cursor Bugbot

Details

Your team is on the Bugbot Free tier. On this plan, Bugbot will review limited PRs each billing cycle for each member of your team.

To receive Bugbot reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Copy link
Contributor

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

3 issues found across 3 files

Prompt for AI agents (all 3 issues)

Understand the root cause of the following 3 issues and fix them.


<file name="packages/nodes-base/nodes/Transform/Aggregate/Aggregate.node.ts">

<violation number="1" location="packages/nodes-base/nodes/Transform/Aggregate/Aggregate.node.ts:486">
Destination field conflict detection only checks exact name matches, so grouping by nested fields (e.g. `user.country`) and outputting to the parent key (`user`) overwrites the group key in the result. Use a prefix-aware comparison to block parent/child path conflicts.</violation>

<violation number="2" location="packages/nodes-base/nodes/Transform/Aggregate/Aggregate.node.ts:570">
Aggregated field conflict detection also checks only exact names, so nested paths like `user` vs `user.country` overwrite each other and drop the recorded group value. Block parent/child overlaps between group-by fields and aggregated output fields before writing them.</violation>

<violation number="3" location="packages/nodes-base/nodes/Transform/Aggregate/Aggregate.node.ts:645">
The implementation for &#39;pairedItem&#39; in grouped aggregation only preserves the data lineage for the first item in each group, discarding the link to all other items. This contradicts the established pattern of using a `pairedItem` array for aggregated items and breaks the expectation of full data traceability, which is a key architectural feature.</violation>
</file>

Reply to cubic to teach it or ask questions. Re-run a review with @cubic-dev-ai review this PR

Adds a new function `hasNestedPathConflict` to accurately detect conflicts between destination/output fields and group by fields when nested paths are involved. This enhances the robustness of the Aggregate node's validation, preventing ambiguous or incorrect error messages and ensuring proper handling of complex field structures.

Includes comprehensive unit tests to cover various conflict scenarios, including parent-child relationships and non-nested shared prefixes.
@pemontto
Copy link
Contributor Author

Reiterating, I'd like to clarify the intended behaviour when aggregating data. Currently, mapping all relevant source items to the pairedItem array (which seems semantically correct) breaks the standard $('node').item syntax, resulting in the "Multiple matching items" error shown above.

The Conflict:

  • Map All Items: Data lineage is accurate, but it forces the UI to treat matches as ambiguous, requiring .first() or .all() syntax (which, as tested, returns unrelated items).
  • Map First Item: The standard .item shorthand works, but the lineage is technically incomplete.

Is there a functional advantage to mapping all items right now if it renders the standard .item selector unusable? Is there something I'm missing or something on the roadmap to handle this?

Specific Context/Use Case:
I realise I never gave a realistic example - the grouping feature itself is incredibly useful for standard 1-to-many workflows, such as Incidents -> Alerts or Jira Issues -> Comments.

  1. Fetch multiple parent items (Incidents).
  2. Fetch related child items (Alerts) for each parent.
  3. Use this node to aggregate the Alerts back into their specific Incident context.

@n8n-assistant n8n-assistant bot added community Authored by a community member node/improvement New feature or request in linear Issue or PR has been created in Linear for internal review labels Nov 21, 2025
@n8n-assistant
Copy link

n8n-assistant bot commented Nov 21, 2025

Hey @pemontto,

Thank you for your contribution. We appreciate the time and effort you’ve taken to submit this pull request.

Before we can proceed, please ensure the following:
• Tests are included for any new functionality, logic changes or bug fixes.
• The PR aligns with our contribution guidelines.

Regarding new nodes:
We no longer accept new nodes directly into the core codebase. Instead, we encourage contributors to follow our Community Node Submission Guide to publish nodes independently.

If your node integrates with an AI service that you own or represent, please email [email protected] and we will be happy to discuss the best approach.

About review timelines:
This PR has been added to our internal tracker as "GHC-5584". While we plan to review it, we are currently unable to provide an exact timeframe. Our goal is to begin reviews within a month, but this may change depending on team priorities. We will reach out when the review begins.

Thank you again for contributing to n8n.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

community Authored by a community member in linear Issue or PR has been created in Linear for internal review node/improvement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant