n8n: take a webhook payload and append it to a CSV file
A form, a sensor, or another service POSTs JSON to your n8n instance, and each payload becomes one row in a CSV file on disk. No database, no third-party service, no credentials. The workflow file below is the exact file that was imported and run for this guide.
On this page
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 test request.
Key result: after step 7, a POST to http://localhost:5678/webhook/signup appends one row to signups.csv and answers {"status":"saved","name":"Ada Lovelace","email":"ada@example.com"} with HTTP 200.
The workflow at a glance
Node versions in the file: Webhook 2.2, Edit Fields 3.5, Code 2, Convert to File 1.2, Read/Write Files 1.1.
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 path used on the test machine (change it in step 4).
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 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 the file with its header row now — the workflow appends data rows only, so the header has to exist first.
Command
mkdir -p ~/.n8n-files
printf 'name,email,received_at\n' > ~/.n8n-files/signups.csv
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 six nodes in a line: Webhook, Edit Fields, Code, Convert to File, Read/Write Files, Edit Fields.
What each node does:
- On signup (Webhook). A trigger node: it starts the workflow when something POSTs to it. HTTP method POST, path
signup, Respond set to When Last Node Finishes, so the caller gets the last node's output back (n8n Docs, Webhook). - Clean up fields (Edit Fields). Keeps three fields and drops the rest:
namefrom{{ $json.body.name }},emailfrom{{ $json.body.email }}, andreceived_atfrom{{ $now.toISO() }}. The webhook delivers the posted JSON underbody, alongside headers, params, and query. Include other fields is off, so none of that reaches the file. - Build CSV line (Code). Runs once for all items — the default Code mode (n8n Docs, Code) — and turns the fields into one CSV line with a trailing newline, quoting any field that contains a comma, a quote, or a newline:
Config — Build CSV line, JavaScript
const esc = (v) => {
const s = String(v ?? '');
return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
};
const lines = $input.all().map(({ json }) =>
[json.name, json.email, json.received_at].map(esc).join(','),
);
return [{ json: { line: lines.join('\n') + '\n' } }];
- To text file (Convert to File). Operation Convert to Text File, source field
line, with Add BOM off. It turns the text into a binary file the next node can write (n8n Docs, Convert to File). - Append to CSV (Read/Write Files). Operation Write File to Disk, with the Append option on, which appends to the existing file instead of creating a new one (n8n Docs, Read/Write Files from Disk).
- Reply (Edit Fields). Returns
status(saved),name, andemailto the caller. It reads the name and email from the earlier node by name —{{ $('Clean up fields').first().json.name }}— because the Convert to File node drops the input JSON (see the traps).
4. Set the file path
Open the Append to CSV node and set File Path and Name to the absolute path of the file you made in step 2 — for example /home/alex/.n8n-files/signups.csv. The downloaded file carries the path used on the test machine (/home/hatch/.n8n-files/signups.csv); change it to yours. Keep the file under ~/.n8n-files unless you have changed N8N_RESTRICT_FILE_ACCESS_TO, or the write is refused.
Trap — environment variables are denied in expressions. The first version of this workflow used {{ $env.HOME }}/.n8n-files/signups.csv as the path. The run failed with NodeApiError: access to env vars denied at the Read/Write Files node. n8n blocks $env in expressions by default. Use a literal absolute path instead.
5. Activate it
Click Publish (the workflow must be active). The Webhook node has two URLs: a test URL and a production URL. The production URL is only registered when the workflow is published (n8n Docs, Webhook). For this workflow, the production URL is:
Config — production webhook URL
http://localhost:5678/webhook/signup
6. Send a test payload
Send a POST with a JSON body. This is the exact request used to test the guide:
Command
curl -X POST http://localhost:5678/webhook/signup \
-H 'Content-Type: application/json' \
-d '{"name":"Ada Lovelace","email":"ada@example.com"}'
Output
{"status":"saved","name":"Ada Lovelace","email":"ada@example.com"}
The response is HTTP 200. Send a second payload to prove the append works:
Command
curl -X POST http://localhost:5678/webhook/signup \
-H 'Content-Type: application/json' \
-d '{"name":"Grace Hopper","email":"grace@example.com"}'
Output
{"status":"saved","name":"Grace Hopper","email":"grace@example.com"}
7. Check the file
Command
cat ~/.n8n-files/signups.csv
Output
name,email,received_at
Ada Lovelace,ada@example.com,2026-11-09T16:32:05.281-05:00
Grace Hopper,grace@example.com,2026-11-09T16:32:07.519-05:00
Key result: two POSTs, two rows. Both executions finished with status success in n8n, and the file holds the header plus one row per payload. The file was checked byte by byte (cat -A showed each row ending with a newline; od -c showed no byte-order mark at the start of the rows).
Traps
Trap — Convert to File's CSV mode is wrong for appending. The first version of this workflow used Convert to File's Convert to CSV operation. Two things went wrong, both visible in the file: each chunk started with a byte-order mark, and there was no newline at the end of a row, so the second row was glued to the end of the first (...16:31:07.869-05:00Grace Hopper,...). Build the line in a Code node and convert it with Convert to Text File and Add BOM off, as in step 3.
Trap — Convert to File drops the input JSON. After the Convert to File node, the item JSON is empty ({}), so a later node that reads {{ $json.name }} gets null — the first test run answered {"status":"saved","name":null,"email":null} even though the row was written correctly. Reference the earlier node by name instead: {{ $('Clean up fields').first().json.name }}.
Trap — the header row must already exist. The workflow appends data rows only. If signups.csv does not exist, the first run creates it without a header, and the file has no column names. Make the file with the header first, as in step 2.
Trap — the test webhook URL is not the production URL. The Webhook node shows a test URL and a production URL. The test URL only works while you are testing in the editor; the production URL works once the workflow is published. Send real traffic to the production URL (/webhook/signup, not /webhook-test/signup).
Summary
A Webhook node receives the POST, an Edit Fields node keeps three fields and adds a timestamp, a Code node builds one CSV line, Convert to File turns it into a file, Read/Write Files appends it to signups.csv, and a final Edit Fields node answers the caller. Two test payloads produced two rows, each on its own line, with no stray bytes. The two mistakes that cost time during the build — environment variables denied in expressions, and Convert to File's CSV mode adding a byte-order mark with no trailing newline — are both in the traps above, with the working fix in the workflow file.
Sources
- n8n Docs, Webhook node — test vs production URLs, Respond modes, the 16 MB payload limit
- n8n Docs, Convert to File node — Convert to CSV and Convert to Text File operations
- n8n Docs, Read/Write Files from Disk node — Write File to Disk, the Append option, and the
~/.n8n-filesdefault in n8n 2.0 - n8n Docs, Code node — Run Once for All Items mode, and that the Code node cannot access the file system
- n8n Docs, Install with npm — the
npx n8nstart, the Node.js 20.19–24.x requirement, and that npm installs are deprecated from n8n 3.0
Verified 9 November 2026 on n8n 2.46.7 (Node.js 24.22.1) by importing workflow.json, publishing it, and sending the two curl requests in step 6. Both executions finished with status success; the file contents in step 7 are the actual output.