Skip to content

n8n: convert a CSV file to JSON without leaving the machine

Checked 10 November 2026 · n8n 2.46.7 · Node.js 24.22.1 · 8 min read

A CSV file sits on your machine and you need it as JSON — for an API, a script, or another tool that speaks JSON and not CSV. This workflow reads the file, parses it properly (a field that contains a comma stays one field), and writes the result out as a JSON file next to it. No database, no third-party service, no credentials. The workflow file below is the exact file that was imported and run for this guide.

The workflow at a glance

Node versions in the file: Manual Trigger 1, Read/Write Files 1.1, Code 2, Convert to File 1.2.

Key result: after step 5, orders.csv (3 rows, one item named Keyboard, mechanical) becomes orders.json holding exactly those 3 rows as JSON, with the quoted comma intact. Running it a second time overwrites the file — it still holds 3 rows, not 6.

Download the workflow: workflow.json — import it into n8n in step 3. It is the exact file that was imported and run for this guide, including the file paths used on the test machine (change them in step 4).

What you need

  • Node.js 20.19 to 24.x. n8n requires a Node.js version between 20.19 and 24.x, inclusive (n8n Docs, Install with npm). This guide ran on Node.js 24.22.1.
  • n8n 2.46.7, started on your own machine. No account, no cloud service, no credentials.
  • A terminal for the install, the file setup, and the check in step 6.
  • A CSV file to convert. Step 2 makes a small orders file so you can see the parsing work; swap in your own file afterwards.

1. Install and start n8n

Run n8n from the terminal. n8n Docs, Install with npm describes this as the quick way to try n8n locally: the command downloads what it needs and serves the editor at http://localhost:5678.

Command

npx n8n

Open http://localhost:5678. The first start asks you to create an owner account (an email address and a password, kept on your own machine). Note that npm-based installs are deprecated from n8n 3.0, per the same install page — if you are on 3.0 or later, use the Docker install it links to instead, and re-check the node versions above before importing.

2. Make the CSV file

On a default n8n 2 install, the Read/Write Files node can only read and write under ~/.n8n-files. That is the default value of N8N_RESTRICT_FILE_ACCESS_TO from n8n 2.0 (n8n Docs, Read/Write Files from Disk). Create the folder and a sample file with three orders. One item name contains a comma inside quotes, and one contains an escaped quote — both are there on purpose, so step 6 proves the parser handled them instead of splitting on every comma.

Command

mkdir -p ~/.n8n-files
printf 'order_id,item,quantity,price\n1001,"Keyboard, mechanical",2,89.99\n1002,Mouse,1,24.50\n1003,"Monitor 27""",1,199.00\n' > ~/.n8n-files/orders.csv

Output — cat ~/.n8n-files/orders.csv

order_id,item,quantity,price
1001,"Keyboard, mechanical",2,89.99
1002,Mouse,1,24.50
1003,"Monitor 27""",1,199.00

3. Import the workflow

In n8n, open the workflow menu (the ⋯ at the top right of the canvas) and choose Import from file, then pick workflow.json. The canvas shows five nodes in a line: Manual Trigger, Read/Write Files, Code, Convert to File, Read/Write Files.

What each node does:

NodeTypeJob
Run manuallyManual TriggerA trigger node: it starts the workflow when you click Execute Workflow in the editor, and never runs on its own. The Manual Trigger node is for workflows that do not have an automatic trigger (n8n Docs, Manual Trigger). This workflow has no schedule and no webhook, so manual runs are the intended way to run it — and a Manual Trigger workflow does not need publishing.
Read orders CSVRead/Write FilesOperation Read File(s) From Disk, file /home/hatch/.n8n-files/orders.csv. It loads the file as binary data the next node can read (n8n Docs, Read/Write Files from Disk).
Parse CSV to JSONCodeRuns once for all items — the default Code mode (n8n Docs, Code) — and turns the CSV text into one JSON item per row, using the header row as the field names. The parser is a small state machine, not a split on commas: a quoted field may contain commas, escaped quotes (""), and line breaks. The Code node cannot reach the file system itself, so it reads the file bytes through this.helpers.getBinaryDataBuffer (see the traps — the obvious way to get the text does not work).

Config — Parse CSV to JSON, JavaScript

// Read the CSV text the Read node loaded, then parse it into one JSON
// item per row. Quoted fields may contain commas, escaped quotes (""),
// and line breaks, so a split on commas would break them.
//
// Binary data is stored on disk in filesystem mode, so the item only
// carries the marker "filesystem-v2" in its data field. The bytes come
// from this.helpers.getBinaryDataBuffer, which reads them from the store.
const buffer = await this.helpers.getBinaryDataBuffer(0, 'data');
const text = buffer.toString('utf8');

function parseCsv(input) {
  const rows = [];
  let row = [];
  let field = '';
  let inQuotes = false;
  for (let i = 0; i < input.length; i++) {
    const ch = input[i];
    if (inQuotes) {
      if (ch === '"') {
        if (input[i + 1] === '"') { field += '"'; i++; }
        else { inQuotes = false; }
      } else { field += ch; }
    } else if (ch === '"') {
      inQuotes = true;
    } else if (ch === ',') {
      row.push(field); field = '';
    } else if (ch === '\r') {
      // Ignore carriage returns; the newline below ends the row.
    } else if (ch === '\n') {
      row.push(field); field = '';
      rows.push(row); row = [];
    } else { field += ch; }
  }
  if (field !== '' || row.length > 0) { row.push(field); rows.push(row); }
  return rows;
}

const parsed = parseCsv(text);
const headers = parsed[0];
const rows = parsed.slice(1).filter((r) => r.some((v) => v !== ''));
return rows.map((r) => {
  const json = {};
  headers.forEach((h, i) => { json[h] = r[i] ?? ''; });
  return { json };
});
NodeTypeJob
To JSON fileConvert to FileOperation Convert to JSON, mode All Items to One File, with Format on so the file is indented and readable. It turns the three JSON items into one binary JSON file (n8n Docs, Convert to File).
Write orders JSONRead/Write FilesOperation Write File to Disk, file /home/hatch/.n8n-files/orders.json, with the Append option off. Append off means it creates a new file instead of appending to an existing one (n8n Docs, Read/Write Files from Disk) — so a second run replaces the file rather than doubling the rows.

4. Set the file paths

Open the Read orders CSV node and set File(s) Selector to the absolute path of your CSV file, and open the Write orders JSON node and set File Path and Name to the absolute path where the JSON should go. The downloaded file carries the paths used on the test machine (/home/hatch/.n8n-files/orders.csv and /home/hatch/.n8n-files/orders.json); change both to yours. Keep the files under ~/.n8n-files unless you have changed N8N_RESTRICT_FILE_ACCESS_TO, or the read and the write are refused.

TRAPenvironment variables are denied in expressions. The same restriction that bit the webhook-to-CSV guide applies here: n8n blocks $env in expressions by default, so {{ $env.HOME }}/.n8n-files/orders.csv fails with NodeApiError: access to env vars denied. Use a literal absolute path instead.

5. Run it

Click Execute Workflow in the editor (or Execute step on the Manual Trigger node). The workflow runs once and stops — there is nothing to publish and no schedule to set, because a Manual Trigger workflow only runs when you tell it to. Run it a second time afterwards: the second run proves the write overwrites instead of appending.

6. Check the file

Command

cat ~/.n8n-files/orders.json

Output

[
  {
    "order_id": "1001",
    "item": "Keyboard, mechanical",
    "quantity": "2",
    "price": "89.99"
  },
  {
    "order_id": "1002",
    "item": "Mouse",
    "quantity": "1",
    "price": "24.50"
  },
  {
    "order_id": "1003",
    "item": "Monitor 27\"",
    "quantity": "1",
    "price": "199.00"
  }
]

Check it parses, and that the second run did not add rows:

Command

python3 -c "import json; rows = json.load(open('/home/hatch/.n8n-files/orders.json')); assert len(rows) == 3; assert rows[0]['item'] == 'Keyboard, mechanical'; print('valid JSON, 3 rows, quoted comma intact')"

Output

valid JSON, 3 rows, quoted comma intact

Key result: two manual runs, both finished with status success in n8n. After the first run the file held 3 rows (313 bytes); after the second run it still held 3 rows with the same checksum, so the write overwrote rather than appended. The quoted comma in Keyboard, mechanical survived as one field, and the escaped quote in Monitor 27" came through as a single ".

Traps

TRAPthe binary data field holds a marker, not the file text. The first version of this workflow read the CSV with Buffer.from($input.first().binary.data.data, 'base64'). The run reported success — and produced nothing: the Code node returned zero items and the write node never ran. The item's data field held the string filesystem-v2, not base64, because n8n 2.46.7 stores binary data on disk. Decoding the marker as base64 gave garbage with no newline, the parser saw a header-only file, and the row filter returned an empty list. Read the bytes with await this.helpers.getBinaryDataBuffer(0, 'data') instead, as in step 3 — it works in the task-runner Code node too.
TRAPa successful run can still write nothing. The failed first run above finished with status success in n8n. The Code node returning an empty list is not an error to n8n — the later nodes simply never run. If your output file is missing after a green run, check the Code node's output item count first, not the execution status.
TRAPvalues stay strings. The parser keeps every field as a string: quantity is "2", not 2, and price is "89.99", not 89.99. That is the honest output for a CSV, which has no types — but if the tool reading the JSON needs numbers, convert them in the Code node (Number(json.quantity)) or in the consumer, and re-check the file after the change.

End state

A Manual Trigger node starts the workflow on demand, a Read/Write Files node reads orders.csv, a Code node parses it into one JSON item per row with real quoted-field handling, Convert to File turns the rows into one formatted JSON file, and a second Read/Write Files node writes orders.json, overwriting on each run. Two manual runs produced the same 3-row file, byte for byte. The mistake that cost time during the build — treating the binary data field as base64 when it holds a filesystem marker — is in the traps above, with the working fix in the workflow file.

Sources

Verified 10 November 2026 on n8n 2.46.7 (Node.js 24.22.1) by importing workflow.json and running it manually twice (executions 20 and 23, both status success; run 1 started 2026-11-10T15:12:44.934Z, run 2 started 2026-11-10T15:13:14.553Z). The file contents in step 6 are the actual output: 3 rows, 313 bytes, md5 93fbb38aada58c6e1a1b3d1ac4ccde48 after both runs.