5 Data Cleanup Problems Every Make.com Scenario Hits (and One-Operation Fixes)
Five one-operation JavaScript fixes for Make.com data transformation: broken AI JSON, group and sum arrays, localized numbers, timezones, file to Base64.
"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.
customer.name, mapped arrays that arrive as JSON strings, and comma-decimal amounts like "19,90".returns is mappable in the next module, so the rest of the scenario stays visual Make.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.
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 size | One sum (Iterator + Aggregator) | Three metrics (three passes) | One JS step |
|---|---|---|---|
| 50 items | ~51 operations | ~153 operations | 1 operation |
| 200 items | ~201 operations | ~603 operations | 1 operation |
| 1,000 items | ~1,001 operations | ~3,003 operations | 1 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 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:
input variable.typeof input === "string" ? JSON.parse(input) : input.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.
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.
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.

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.
"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.
A fair guide draws the boundary honestly, and the boundary here is real. Stick with the native modules when:
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.
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.
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
Continue reading on similar topics
Five one-operation JavaScript fixes for Make.com data transformation: broken AI JSON, group and sum arrays, localized numbers, timezones, file to Base64.
Call any API from Make.com: WebSockets, Digest auth, OAuth 1.0 signatures, and Base64 binaries with copy-paste JavaScript that runs inside your scenario.
Polling a mailbox every 15 minutes costs 2,880 Make.com operations a month before processing a single email. The full cost math for Make.com and n8n Cloud, and how a push mailhook fixes cost and latency at once.