FlowRunbook

Automation runbooks that actually run — every workflow tested, with the file to download.

n8n: take a webhook payload and append it to a CSV file

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

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.

What you need

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:

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' } }];

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

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.