n8n: route a webhook by event type into two different files
One webhook receives every event, and an IF node sends each one down one of two paths: a purchase goes to a purchases file, anything else goes to a signups file. 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: Webhook 2.2, IF 2.2, Edit Fields 3.5, Code 2, Convert to File 1.2, Read/Write Files 1.1. Rows 3 to 6 run only when the event type is purchase; rows 7 to 10 run for anything else. Row 11 runs on either path.
Key result: after step 7, a POST to http://localhost:5678/webhook/events with {"type":"purchase",...} appends one row to events-purchases.csv and nothing to the signups file, while a signup payload does the reverse. Each call answers with HTTP 200 and the file it wrote to.
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 test requests.
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 files
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 both files with their header rows now — the workflow appends data rows only, so the headers have to exist first. The two files have different columns, because a purchase carries an item and an amount while a signup carries a name.
Command
mkdir -p ~/.n8n-files
printf 'type,name,email\n' > ~/.n8n-files/events-signups.csv
printf 'type,item,amount,email\n' > ~/.n8n-files/events-purchases.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 eleven nodes: a Webhook, an IF node, two branches of four nodes each, and a shared Reply node at the end.
What the shared head does:
| Node | Type | Job |
|---|---|---|
| On events | Webhook | A trigger node: it starts the workflow when something POSTs to it. HTTP method POST, path events, Respond set to When Last Node Finishes, so the caller gets the last node's output back (n8n Docs, Webhook). |
| Route by type | IF | Splits the workflow on one condition: {{ $json.body.type }} is equal to purchase. Items that meet the condition leave through the true output; everything else leaves through the false output (n8n Docs, If node). The comparison is the string is equal to, case-sensitive (Ignore Case off), so Purchase with a capital P does not match. The IF node has two outputs, and both are wired — an item routed to an unwired output is dropped with no error, so check both branches before you publish. |
The true branch keeps the purchase fields and writes them. The false branch does the same for signups:
| Node | Type | Job |
|---|---|---|
| Pick purchase fields | Edit Fields | Keeps four fields and drops the rest: type from {{ $json.body.type }}, item from {{ $json.body.item }}, amount from {{ $json.body.amount }} (a number), and email from {{ $json.body.email }}. Include other fields is off (n8n Docs, Edit Fields (Set)). |
| Build purchase 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 purchase line, JavaScript
const esc = (v) => {
const s = String(v ?? '');
return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
};
const lines = $input.all().map(({ json }) =>
[json.type, json.item, json.amount, json.email].map(esc).join(','),
);
return [{ json: { line: lines.join('\n') + '\n' } }];
| Node | Type | Job |
|---|---|---|
| Pick signup fields | Edit Fields | Keeps three fields and drops the rest: type from {{ $json.body.type }}, name from {{ $json.body.name }}, and email from {{ $json.body.email }}. Include other fields is off. |
| Build signup line | Code | Same shape as the purchase line, with the signup columns: |
Config — Build signup line, JavaScript
const esc = (v) => {
const s = String(v ?? '');
return /[",\n]/.test(s) ? '"' + s.replace(/"/g, '""') + '"' : s;
};
const lines = $input.all().map(({ json }) =>
[json.type, json.name, json.email].map(esc).join(','),
);
return [{ json: { line: lines.join('\n') + '\n' } }];
Both branches end the same way, and both feed one shared Reply node:
| Node | Type | Job |
|---|---|---|
| To text file (purchases) | 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 purchases | 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). |
| To text file (signups) | Convert to File | Same operation as the purchases side, with the file name events-signups.csv. |
| Append signups | Read/Write Files | Same operation as the purchases side, writing to the signups file. |
| Reply | Edit Fields | Returns status (saved), type, file, and email to the caller. It reads all three from the Webhook node by name — {{ $('On events').first().json.body.type }} — because the Convert to File node drops the input JSON (the earlier webhook guide recorded the same behaviour: after Convert to File, the item JSON is empty, so a later node reading {{ $json.name }} gets null). The file value is a short expression on the type: purchase gives events-purchases.csv, anything else gives events-signups.csv. |
4. Set the file paths
Open the Append purchases node and set File Path and Name to the absolute path of the purchases file you made in step 2, then do the same in Append signups — for example /home/alex/.n8n-files/events-purchases.csv and /home/alex/.n8n-files/events-signups.csv. The downloaded file carries the paths used on the test machine (/home/hatch/.n8n-files/events-purchases.csv and /home/hatch/.n8n-files/events-signups.csv); change both to yours. Keep the files under ~/.n8n-files unless you have changed N8N_RESTRICT_FILE_ACCESS_TO, or the write is refused. Use literal absolute paths: n8n blocks $env in expressions by default, so {{ $env.HOME }}/.n8n-files/... fails with access to env vars denied (recorded in the earlier webhook guide).
| TRAP | two nodes with the same name break the wiring. The first draft of this workflow named both Convert to File nodes To text file. n8n connections are by node name, so both branches pointed at the same node and the routing was wrong. The draft was caught before import: the two nodes are named To text file (purchases) and To text file (signups) in the shipped file. Give every node a distinct name, especially when a workflow repeats a node type. |
|---|
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/events
6. Send test payloads
Send one POST per event type. These are the exact requests used to test the guide — a signup first, then a purchase:
Command
curl -X POST http://localhost:5678/webhook/events \
-H 'Content-Type: application/json' \
-d '{"type":"signup","name":"Ada Lovelace","email":"[email protected]"}'
Output
{"status":"saved","type":"signup","file":"events-signups.csv","email":"[email protected]"}
Command
curl -X POST http://localhost:5678/webhook/events \
-H 'Content-Type: application/json' \
-d '{"type":"purchase","item":"Keyboard","amount":79,"email":"[email protected]"}'
Output
{"status":"saved","type":"purchase","file":"events-purchases.csv","email":"[email protected]"}
Both responses are HTTP 200. The file field in each answer names the file the event went to — that is the routing, visible in the response.
7. Check the files
Command
cat ~/.n8n-files/events-signups.csv
cat ~/.n8n-files/events-purchases.csv
Output
type,name,email
signup,Ada Lovelace,[email protected]
type,item,amount,email
purchase,Keyboard,79,[email protected]
Key result: two POSTs, two rows, each in only the correct file. The signup row is in events-signups.csv and nowhere else; the purchase row is in events-purchases.csv and nowhere else. Both executions finished with status success in n8n, and the files were checked byte by byte (od -c showed each row ending with a newline and no byte-order mark at the start of either file).
Traps
| TRAP | two nodes with the same name break the wiring. Covered in step 4: n8n connections are by node name. The first draft of this workflow gave both Convert to File nodes the name To text file, which pointed both branches at one node. Name repeated node types distinctly — here To text file (purchases) and To text file (signups). |
|---|---|
| TRAP | anything that is not exactly purchase takes the false branch. The IF node has one condition and two outputs: a payload with type set to refund, a typo like purchse, or no type at all — all land in the signups file. That is the design — the false branch is the catch-all — but it means a misspelled type silently files a purchase as a signup instead of failing. If you need three or more routes, use the Switch node instead: the IF docs say so directly (n8n Docs, If node). |
| TRAP | the REST API refuses active in a create call. A round-trip test of the shipped file against the n8n API returned HTTP 400 with {"message":"request/body/active is read-only"}. The file imports fine through the editor's Import from file (step 3), which is the path this guide uses. If you create workflows through the API instead, strip the active key from the body and activate with the /activate endpoint afterwards. |
| 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 earlier webhook guide recorded a first test run answering {"status":"saved","name":null,"email":null} even though the row was written correctly. The Reply node here reads from the Webhook node by name instead: {{ $('On events').first().json.body.email }}. |
| TRAP | the header rows must already exist. The workflow appends data rows only. If either CSV file does not exist, the first run creates it without a header, and the file has no column names. Make both files with their headers 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/events, not /webhook-test/events). |
End state
A Webhook node receives the POST, an IF node sends purchases down the true branch and everything else down the false branch, each branch keeps its own fields, builds one CSV line, converts it to a file, and appends it to its own CSV. A shared Reply node answers the caller with the file the event went to. Two test payloads produced two rows, each in only the correct file. The two mistakes that cost time during the build — giving both Convert to File nodes the same name, and the REST API refusing the active field on create — are both in the traps above, with the working fix in the workflow file.
Sources
- n8n Docs, If node — split a workflow conditionally, the string "is equal to" comparison, and that more than two outputs calls for the Switch node
- n8n Docs, Webhook node — test vs production URLs, Respond modes, the 16 MB payload limit
- n8n Docs, Edit Fields (Set) node — Manual Mapping and Keep Only Set Fields
- n8n Docs, Code node — Run Once for All Items mode, and that the Code node cannot access the file system
- 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, 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 10 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. Execution 12 (signup, started 15:06:45.017Z) and execution 13 (purchase, started 15:06:45.854Z) both finished with status success; the file contents in step 7 are the actual output. The shipped file was also re-imported as a throwaway copy (11 nodes, HTTP 200) and deleted again.