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.
Updated October 2026
To parse CSV in Make, use the built-in CSV app, module "Parse CSV" when the file has a fixed number of columns and a known delimiter, and use a 25-line JavaScript step when it does not. The code version below autodetects comma or semicolon, handles quoted fields with embedded delimiters, escaped quotes and line breaks, uses the header row as object keys, drops trailing empty lines and strips a UTF-8 BOM, then returns a plain array that an Iterator or the next module can map directly.
Parsing or building delimited text is the second most common job in our customers' inline scripts (43% of them do one or the other). Most started with the built-in module and hit one of four walls: the column count changes between files, the exporter writes semicolons and a BOM, the output fields are named col1, col2 instead of the header names, or the numbers arrive as "9,50". This guide covers both directions in Make and in n8n.
1.price style paths directly if you only need a few values.json2csv call with the json-2-csv package, available on every CustomJS plan, or nine lines of hand-rolled join with proper quoting.Make ships a CSV app with three modules: Parse CSV (a transformer that turns CSV text into one bundle per row), Create CSV (an aggregator that joins bundles into CSV text) and Create CSV (advanced), which uses a Make data structure to define the columns. Parse CSV has four settings: Number of columns, CSV contains headers, Delimiter (comma, tab, or "other" with a character you type, which is how you get semicolons) and, under advanced settings, Preserve quotes inside unquoted field. Facts in this section were checked in October 2026 against the module configuration and Make community threads, since Make's help page for the CSV app has moved and now redirects.
For a clean file it does the job: quoted cells with commas inside are handled, the header row is skipped when you tick the box, and every row comes out as its own bundle. Where it stops:
Number of columns on line 1 does not match header and the whole run stops.Invalid Opening Quote on col1 unless you enable "Preserve quotes inside unquoted field"."9,50" is a string with a comma in it. Make's math functions need a number with a dot. Add the CustomJS module Execute Inline JavaScript, map the raw CSV text into Input (the body of a Fetch a File module, an email attachment converted with toString(), or a webhook field), set Return Type to Array, and paste this into the code field. The text arrives in the script as the input variable.
Sample input, the kind of export a European shop system produces: semicolons, a BOM at the very start, a product name with commas and a quoted word inside, a multi-line cell, and two blank lines at the end.
sku;name;price;qty
A-1;"Mug, ""blue"", 350 ml";9,50;12
B-2;Plate;12,00;4
C-3;"Bowl
large";1.249,00;0
The parser:
// input = the raw CSV text (map the file's data or the email body here)
const text = (typeof input === "string" ? input : String(input))
.replace(/^\uFEFF/, ""); // strip a UTF-8 BOM
const head = text.split(/\r?\n/)[0];
const delim = (head.split(";").length > head.split(",").length) ? ";" : ",";
const rows = [];
let row = [], cell = "", quoted = false;
for (let i = 0; i < text.length; i++) {
const ch = text[i];
if (quoted) {
if (ch === '"' && text[i + 1] === '"') { cell += '"'; i++; } // "" -> "
else if (ch === '"') quoted = false;
else cell += ch; // delimiters and newlines kept
} else if (ch === '"') quoted = true;
else if (ch === delim) { row.push(cell); cell = ""; }
else if (ch === "\n" || ch === "\r") {
if (ch === "\r" && text[i + 1] === "\n") i++;
row.push(cell); rows.push(row); row = []; cell = "";
} else cell += ch;
}
if (cell !== "" || row.length) { row.push(cell); rows.push(row); }
const [header, ...body] = rows.filter(r => r.some(c => c.trim() !== ""));
return body.map(r =>
Object.fromEntries(header.map((h, i) => [h.trim(), (r[i] ?? "").trim()]))
);Output, as the Make mapping panel shows it after the module runs:
[
{ "sku": "A-1", "name": "Mug, \"blue\", 350 ml", "price": "9,50", "qty": "12" },
{ "sku": "B-2", "name": "Plate", "price": "12,00", "qty": "4" },
{ "sku": "C-3", "name": "Bowl\nlarge", "price": "1.249,00", "qty": "0" }
] The delimiter guess compares the count of semicolons and commas on the first line; a header row almost never contains the other character. If your source is tab-separated, replace the ternary with const delim = "\t";. The character loop is the actual CSV state machine: inside quotes everything is literal, a doubled quote becomes one quote, and a delimiter or newline outside quotes ends the cell or the row. Windows \r\n line endings are handled, and the final filter drops any line where every cell is blank, which covers trailing newlines and the empty padding rows Excel leaves behind.
The module call finishes in about 120 ms end to end for a file this size (the median inline script across our customers runs in 121 ms), and because the parser is a single pass over the text a 5,000-row file still stays under a second. Make bills one operation for the module call regardless of the row count.
With Return Type set to Array, Make sees the module output as one bundle holding an array of collections. From there you have two routes:
sku, name, price and qty as named fields. Choose this when every row needs its own action, such as an upsert into Airtable or a Google Sheets row. Cost is one operation per row for the Iterator plus one per row for each module after it, the same as after Parse CSV.get(3.array; 1).sku, or length(3.array) for the row count. No per-row operations at all.JSON.stringify(...) with Return Type Text when the receiver wants a string.If the step after parsing is a group-by or a sum, you do not need the Iterator either: group and sum arrays in Make shows how to do that inside the same code step, or with the no-code Group & Aggregate Array module.
The parser above deliberately leaves every cell as text. Coercing is a second, explicit step, because a SKU like 00123 must stay a string while a price must become a number. Append this to the end of the parser (replace the final return with const rows = ..., then add):
const toNumber = (v) => {
const s = String(v).replace(/\s/g, "");
if (!/^-?\d+([.,]\d{3})*([.,]\d+)?$/.test(s)) return v; // leave text alone
const last = Math.max(s.lastIndexOf(","), s.lastIndexOf("."));
const decimal = last >= 0 && s.length - last - 1 !== 3 ? s[last] : "";
const digits = decimal ? s.replace(new RegExp("[^0-9-" + decimal + "]", "g"), "")
: s.replace(/[.,]/g, "");
return Number(digits.replace(",", "."));
};
return rows.map(r => ({
...r,
price: toNumber(r.price),
qty: toNumber(r.qty),
})); The rule is the same one a human uses: the last separator is the decimal mark unless exactly three digits follow it, in which case it is a thousands separator. So 9,50 becomes 9.5, 1.249,00 becomes 1249, 1,249.00 also becomes 1249, and anything that is not shaped like a number is returned untouched. The one ambiguous case is a bare 1.249 with no decimal part, which this treats as 1249; if your file never has thousands separators, drop the !== 3 check. For single values outside a code step, the Parse Localized Number module applies the same logic with an explicit format selector.
Output after coercion, ready for sum() or a Numeric Aggregator:
[
{ "sku": "A-1", "name": "Mug, \"blue\", 350 ml", "price": 9.5, "qty": 12 },
{ "sku": "B-2", "name": "Plate", "price": 12, "qty": 4 },
{ "sku": "C-3", "name": "Bowl\nlarge", "price": 1249, "qty": 0 }
]
The reverse direction comes up when a scenario collects rows (from an API, a data store, an Array Aggregator) and has to hand someone a file: a daily CSV attached to an email, an upload to SFTP or Google Drive, an import file for a shop. Map the array into Input (if it is a Make array rather than a JSON string, put the JSON app's Transform to JSON module in front, or rely on the defensive parse in line 3), set Return Type to Text, and use the json-2-csv package, which is on the npm module list for every plan including free:
const { json2csv } = require("json-2-csv");
const rows = typeof input === "string" ? JSON.parse(input) : input;
const csv = json2csv(rows, {
delimiter: { field: ";" }, // use "," for US-style files
excelBOM: true, // Excel opens UTF-8 umlauts correctly
});
return csv;Output for the three coerced rows above (the leading BOM is invisible but present):
sku;name;price;qty
A-1;"Mug, ""blue"", 350 ml";9.5;12
B-2;Plate;12;4
C-3;"Bowl
large";1249;0json2csv quotes only the cells that need it (the ones containing the delimiter, a quote or a line break), doubles embedded quotes, and takes the header from the object keys. Version 5.6 of the package returns the string synchronously, so no await is needed. If you would rather not depend on a package, the hand-rolled version is nine lines and produces the same output with Windows line endings:
const rows = typeof input === "string" ? JSON.parse(input) : input;
const delim = ";";
const quote = (v) => {
const s = v == null ? "" : String(v);
const needs = /["\n\r]/.test(s) || s.includes(delim);
return needs ? '"' + s.replace(/"/g, '""') + '"' : s;
};
const keys = Object.keys(rows[0]);
const lines = rows.map(r => keys.map(k => quote(r[k])).join(delim));
return [keys.join(delim), ...lines].join("\r\n"); To attach the result to an email, map the text into the attachment's Data field with toBinary() and give it a .csv file name. For an upload module that expects binary data, set Return Type to Binary instead and return Buffer.from(csv, "utf8").
Yes. n8n's built-in Code node is free and runs JavaScript, so the parser pastes in with two changes: read the text from $input.first().json.data (or whichever field holds the CSV) instead of input, and return the rows as n8n items, return rows.map(r => ({ json: r }));, so each row becomes its own item for the following node. Use the "Run Once for All Items" mode. External npm packages such as json-2-csv are only available in the Code node on self-hosted n8n after you allow them with an environment variable, so on n8n Cloud use the hand-rolled builder above or call the CustomJS REST endpoint from an HTTP Request node.
To be fair to n8n: you may not need code at all. Its core Extract From File node has an "Extract From CSV" operation that reads a binary CSV file into items, with a header row toggle, and the matching Convert to File node has "Convert to CSV" for the other direction (both checked in n8n's documentation, October 2026; they replace the deprecated Spreadsheet File node). They work from binary data, so CSV that arrives as plain text in a webhook field either goes through "Move Base64 String to File" first or straight into the Code node. Delimiter autodetection, European number coercion and BOM handling are where the Code node still earns its place.
| Requirement | Make CSV app (Parse CSV) | Inline JavaScript step | n8n Extract From File |
|---|---|---|---|
| Fixed columns, comma, clean file | Yes, zero code | Yes, but unnecessary | Yes, zero code |
| Semicolon or tab delimiter | Yes, via Delimiter "other" | Yes, autodetected | No delimiter option documented |
| Column count varies between files | No, run fails | Yes | Yes |
| Header names as field names | No, col1, col2, ... | Yes | Yes, with Header Row on |
| Quoted cells with delimiters or line breaks | Yes | Yes | Yes |
| UTF-8 BOM from Excel | Error unless "Preserve quotes" is on | Stripped in line 3 | Not documented |
| "1.249,00" to 1249 | No, stays text | Yes, six-line helper | No, stays text |
| Output shape | One bundle per row | One array (iterate if you want bundles) | One item per row |
| Operations for a 2,000-row file, parse only | 1 plus 2,000 for whatever follows | 1 (plus 1 CustomJS request) | 1 execution |
Do not add a code step when the built-in module already does the job. Parse CSV is the right choice when:
The code step is for the other cases: several sources with different delimiters, files whose column set changes, header names you want to keep, European number formats, a BOM, and large files where you need the array rather than two thousand bundles. One module, one operation, and the parser is the same 25 lines every time, which is why many customers save it as an Execute Stored Function named parseCsv and call it from every scenario that receives a file.
The CustomJS free tier (600 requests per month, no credit card) covers a daily import comfortably. Create an API key, add the Make connection, paste the parser, and your next CSV is an array.
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.
Make's Code app bills 2 credits per second, caps at 30 s and ships npm only on Enterprise. CustomJS bills 1 credit per call with npm and stored functions on every plan. Break-even at 1 to 15 s.
Make's Numeric Aggregator computes one function per module and costs one operation per item. The twelve-line JavaScript fix for group by, max per group, count distinct, dedupe and top N, plus the no-code module.