Blog

Group, Sum and Deduplicate Arrays in Make.com Without Iterators

"Group my orders by customer and sum the totals" sounds like a one-liner. In Make.com it is not. The native answer is an Iterator that explodes your array into bundles, followed by one or more Aggregators that fold the bundles back together, and every one of those bundles consumes an operation from your monthly allowance. A 200-item array costs you 200-plus operations for a single sum, before the scenario has done anything useful with the result.

There is a second way: run the whole aggregation as one small JavaScript step. The array goes in, the grouped and summed result comes out, and the entire thing costs exactly one operation no matter whether the array has 20 items or 20,000.

This guide gives you four copy-paste recipes for the CustomJS Execute JavaScript module: group by a field and sum, compute several aggregates at once, deduplicate before aggregating, and rank groups by an aggregated value. All four work on the same realistic e-commerce dataset, including the messy parts like nested fields and European "19,90" decimal strings.

TL;DR

  • Make's native grouping tool is the Iterator plus Aggregator pattern, and each iterated bundle consumes one operation. Aggregating a 200-item array costs roughly 201 operations per run.
  • A Numeric Aggregator computes one function at a time, so "sum plus average plus count" means repeating the pass and multiplying the cost.
  • One JavaScript step does grouping, summing, deduplication, and sorting in a single operation, regardless of array size.
  • The recipes handle real-world data: dot-notation paths like customer.name, mapped arrays that arrive as JSON strings, and comma-decimal amounts like "19,90".
  • Whatever the script returns is mappable in the next module, so the rest of the scenario stays visual Make.
  • The same snippets run in n8n. Free tier: 600 JavaScript executions per month, no credit card.

How Make Counts Operations When You Aggregate

To see why this matters, follow one array through the native pattern. Say a webhook delivers 200 order objects and you want revenue per customer.

  • The Iterator splits the array into 200 bundles. The Iterator itself consumes one operation.
  • Every module after the Iterator now runs once per bundle. The Numeric Aggregator that folds the bundles back into sums therefore consumes 200 operations, one per bundle it processes.
  • The Numeric Aggregator computes one aggregate function per pass. If you also want the average order value and a count, you repeat the Iterator plus Aggregator pass, or chain additional aggregators that each process every bundle again.
  • Deduplicating before you aggregate is worse still: there is no native "distinct by field" module, so people resolve it with data store lookups or filter gymnastics, each running per bundle.

None of this is a bug. Make's per-bundle pricing model is transparent and fair for routing work, where each item genuinely needs its own downstream action. It just makes pure data-shaping expensive, because the shape of the tool (one module run per item) does not match the shape of the problem (one transformation over the whole array).

Array sizeOne sum (Iterator + Aggregator)Three metrics (three passes)One JS step
50 items~51 operations~153 operations1 operation
200 items~201 operations~603 operations1 operation
1,000 items~1,001 operations~3,003 operations1 operation

Now put those numbers against a real allowance. Make's Core plan is $9/month for 10,000 operations (checked on make.com/en/pricing, August 23, 2026; Make has started labeling operations as credits, and iteration and aggregation both consume them). A scenario that aggregates a 1,000-item feed three ways burns roughly 3,000 operations per run. Three runs and the month is effectively gone. The same scenario with a JavaScript step costs one operation per run, plus whatever your trigger and output modules cost.

The Setup: One Module, One Input Field

The recipes below run in the CustomJS "Execute JavaScript (Inline)" action, available in the public Make app. The mechanics take a minute to learn and never change:

  • Map the array in. Add the module to your scenario, paste a recipe into the code field, and map the array output of the previous module (a webhook body, an HTTP response, the rows from a search module) into the module's input field. Inside the script that data is the input variable.
  • Parse defensively. Depending on how you map it, the array can arrive as a JSON string rather than a live object. Every recipe therefore starts with the same tolerant first line: typeof input === "string" ? JSON.parse(input) : input.
  • Return the result. The code runs in a real Node.js runtime, async/await included, and whatever you return becomes the module's output. Return an object and Make shows its fields in the mapping panel of every following module.

All four recipes work on this dataset. It is deliberately messy in the ways production data is messy: the customer name is nested, the totals are localized strings with comma decimals, and order A-1003 appears twice because the webhook retried.

[
  { "id": "A-1001", "status": "paid", "total": "19,90",
    "customer": { "name": "Acme GmbH", "country": "DE" } },
  { "id": "A-1002", "status": "open", "total": "249,00",
    "customer": { "name": "Beta Logistics", "country": "AT" } },
  { "id": "A-1003", "status": "paid", "total": "99,50",
    "customer": { "name": "Acme GmbH", "country": "DE" } },
  { "id": "A-1003", "status": "paid", "total": "99,50",
    "customer": { "name": "Acme GmbH", "country": "DE" } },
  { "id": "A-1004", "status": "refunded", "total": "12,00",
    "customer": { "name": "Cafe Nord", "country": "DE" } }
]

Each recipe is complete and self-contained on purpose, so you can paste it as-is. The first dozen lines are always the same three helpers: the tolerant parser, a get() function that reads nested fields via dot paths like customer.name, and a toNumber() function that turns "19,90" and "1.249,00" into numbers you can actually add.

Recipe 1: Group by a Field and Sum

The workhorse. Group orders by customer.name and sum the total of each group. Change the two constants at the top to group and sum by any other fields, nested or not.

// --- helpers (same in every recipe) ---
const orders = typeof input === "string" ? JSON.parse(input) : input;

const get = (obj, path) =>
  path.split(".").reduce((v, key) => (v == null ? v : v[key]), obj);

const toNumber = (value) => {
  if (typeof value === "number") return value;
  if (value == null) return 0;
  const s = String(value).trim();
  const normalized = s.includes(",")
    ? s.replace(/\./g, "").replace(",", ".")   // "1.249,00" -> "1249.00"
    : s;
  const n = parseFloat(normalized.replace(/[^0-9.\-]/g, ""));
  return Number.isNaN(n) ? 0 : n;
};
// --- end helpers ---

const GROUP_BY = "customer.name";
const SUM_FIELD = "total";

const groups = new Map();

for (const order of orders) {
  const key = String(get(order, GROUP_BY) ?? "unknown");
  const group = groups.get(key) || { customer: key, orders: 0, revenue: 0 };
  group.orders += 1;
  group.revenue += toNumber(get(order, SUM_FIELD));
  groups.set(key, group);
}

return {
  groups: [...groups.values()].map((g) => ({
    ...g,
    revenue: Math.round(g.revenue * 100) / 100
  }))
};

On the sample data this returns three groups, with Acme GmbH at 218.90 across three orders (the duplicate is still counted here; Recipe 3 fixes that). The groups array is fully mappable downstream: write it to Google Sheets in one step, or feed it into an Iterator if each group needs its own action. Iterating groups instead of raw items is the cheap kind of iteration: five customer groups cost five operations, not two hundred.

Recipe 2: Several Aggregations in One Pass

This is where the native pattern gets painful, because one Numeric Aggregator computes one function. In code, extra metrics are extra lines, not extra passes. This recipe returns revenue, order count, average order value, and the number of distinct statuses per customer, all in the same single operation.

// --- helpers (same in every recipe) ---
const orders = typeof input === "string" ? JSON.parse(input) : input;

const get = (obj, path) =>
  path.split(".").reduce((v, key) => (v == null ? v : v[key]), obj);

const toNumber = (value) => {
  if (typeof value === "number") return value;
  if (value == null) return 0;
  const s = String(value).trim();
  const normalized = s.includes(",")
    ? s.replace(/\./g, "").replace(",", ".")
    : s;
  const n = parseFloat(normalized.replace(/[^0-9.\-]/g, ""));
  return Number.isNaN(n) ? 0 : n;
};
// --- end helpers ---

const groups = new Map();

for (const order of orders) {
  const key = String(get(order, "customer.name") ?? "unknown");
  const g = groups.get(key) ||
    { customer: key, orders: 0, revenue: 0, statuses: new Set() };
  g.orders += 1;
  g.revenue += toNumber(get(order, "total"));
  g.statuses.add(get(order, "status"));
  groups.set(key, g);
}

const round2 = (n) => Math.round(n * 100) / 100;

return {
  groups: [...groups.values()].map((g) => ({
    customer: g.customer,
    orders: g.orders,
    revenue: round2(g.revenue),
    avgOrderValue: round2(g.revenue / g.orders),
    distinctStatuses: g.statuses.size
  }))
};

The Set gives you "count distinct" for free, an aggregation the Numeric Aggregator does not offer at all. Want a min or max? Add g.max = Math.max(g.max || 0, amount) inside the loop. Every metric you add is one more field in the mapping panel and zero additional operations.

Use our Make App to execute JavaScript directly in Make.

Make App

Recipe 3: Deduplicate by a Field Before Aggregating

Webhook retries, overlapping API pages, and re-run scenarios all produce the same bug: duplicate items that silently inflate your sums. Our sample data has order A-1003 twice for exactly this reason. Make has no "distinct by field" module, but in JavaScript it is a Set and a filter.

// --- helpers (same in every recipe) ---
const orders = typeof input === "string" ? JSON.parse(input) : input;

const get = (obj, path) =>
  path.split(".").reduce((v, key) => (v == null ? v : v[key]), obj);

const toNumber = (value) => {
  if (typeof value === "number") return value;
  if (value == null) return 0;
  const s = String(value).trim();
  const normalized = s.includes(",")
    ? s.replace(/\./g, "").replace(",", ".")
    : s;
  const n = parseFloat(normalized.replace(/[^0-9.\-]/g, ""));
  return Number.isNaN(n) ? 0 : n;
};
// --- end helpers ---

const DEDUPE_BY = "id";

// keep the first occurrence of every id, drop the rest
const seen = new Set();
const unique = orders.filter((order) => {
  const key = String(get(order, DEDUPE_BY));
  if (seen.has(key)) return false;
  seen.add(key);
  return true;
});

// aggregate the cleaned list
let revenue = 0;
for (const order of unique) revenue += toNumber(get(order, "total"));

return {
  received: orders.length,
  duplicatesRemoved: orders.length - unique.length,
  orders: unique,
  orderCount: unique.length,
  revenue: Math.round(revenue * 100) / 100
};

On the sample data, duplicatesRemoved is 1 and the revenue drops from an inflated 479.90 to the correct 380.40. Two practical notes: returning duplicatesRemoved gives you a free health metric to alert on, and returning the cleaned orders array means the next module can keep working with deduplicated items. To dedupe and group in one step, run this filter first and then paste the grouping loop from Recipe 1 below it, replacing orders with unique.

Recipe 4: Sort Groups by an Aggregated Value

"Top 5 customers by revenue" is the classic report request, and it is genuinely awkward natively because you would have to aggregate first and then sort the aggregate, another Iterator pass. In code, sorting the groups is one sort() call on the result you already have.

// --- helpers (same in every recipe) ---
const orders = typeof input === "string" ? JSON.parse(input) : input;

const get = (obj, path) =>
  path.split(".").reduce((v, key) => (v == null ? v : v[key]), obj);

const toNumber = (value) => {
  if (typeof value === "number") return value;
  if (value == null) return 0;
  const s = String(value).trim();
  const normalized = s.includes(",")
    ? s.replace(/\./g, "").replace(",", ".")
    : s;
  const n = parseFloat(normalized.replace(/[^0-9.\-]/g, ""));
  return Number.isNaN(n) ? 0 : n;
};
// --- end helpers ---

const groups = new Map();

for (const order of orders) {
  const key = String(get(order, "customer.name") ?? "unknown");
  const g = groups.get(key) || { customer: key, orders: 0, revenue: 0 };
  g.orders += 1;
  g.revenue += toNumber(get(order, "total"));
  groups.set(key, g);
}

const ranked = [...groups.values()]
  .sort((a, b) => b.revenue - a.revenue)   // highest revenue first
  .map((g, i) => ({
    rank: i + 1,
    customer: g.customer,
    orders: g.orders,
    revenue: Math.round(g.revenue * 100) / 100
  }));

return {
  top5: ranked.slice(0, 5),
  all: ranked
};

Map top5 into a Slack message or an email template and you have a leaderboard that costs one operation to compute. Flip the comparison to a.revenue - b.revenue for the bottom performers, or sort by orders instead of revenue for a frequency ranking.

When the Native Iterator and Aggregator Are Fine

A fair guide draws the boundary honestly, and the boundary here is real. Stick with the native modules when:

  • The array is tiny. Iterating 5 items costs 6 operations. Saving 5 operations is not worth adding a code step someone has to read later.
  • You need per-item routing anyway. If every order must individually go through a Router to different follow-up modules, you are paying for the Iterator regardless. Aggregating on the side does not change that.
  • A simple join is all you need. The Text Aggregator turning bundles into a comma-separated string, or the Array Aggregator collecting a few mapped fields, is idiomatic Make. No code beats no code.
  • The team maintaining the scenario does not read JavaScript. A visual Iterator chain that everyone can debug is worth some operations. Keep the code step for the scenarios where the math clearly justifies it.

The switch point is volume times frequency. An hourly scenario over a 200-item array is roughly 145,000 operations per month natively for a single aggregation pass, versus about 720 with a JS step. At that scale the code step is not a style preference, it is the difference between the $9 plan and a much bigger one.

The Same Recipes in n8n

Every snippet above runs unchanged in the CustomJS n8n node, and with minor adjustments in n8n's built-in Code node (there you read items from $input.all() instead of input). The argument shifts, though: self-hosted n8n has no per-operation cost at all, and n8n Cloud bills by workflow executions, not by items processed. So in n8n the reason to collapse an aggregation into one code step is simplicity and debuggability, not money. In Make it is both. For a broader look at where JavaScript fits across the three platforms, see our Make vs Zapier vs n8n comparison.

Conclusion

Make's Iterator and Aggregator are routing tools that get pressed into service as data tools, and the operations meter shows the mismatch: one aggregation over a 200-item array should not cost 200-plus operations. A single JavaScript step turns grouping, summing, deduplicating, and ranking into what they actually are, one transformation over one array, for one operation.

The four recipes above are production-shaped: tolerant input parsing, dot-path field access, comma-decimal handling, and mappable return objects. Paste them into the CustomJS Execute JavaScript module, swap the field names, and they run as-is. Setup is a one-time thing: add the module from the public Make app, connect your API key, and you have 600 free executions per month. If you outgrow that, plans start at $9/month for 100 requests per day, with $29 (500/day) and $99 (5,000/day) tiers above. And once the module is in your scenario, it also covers everything in our custom API calls guide, from WebSockets to OAuth 1.0 signing.

Aggregate arrays free, 600 requests per month

Frequently Asked Questions

Related Articles

Continue reading on similar topics