Skip to content

n8n: route a webhook by event type into two different files

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

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:

NodeTypeJob
On eventsWebhookA 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 typeIFSplits 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:

NodeTypeJob
Pick purchase fieldsEdit FieldsKeeps 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 lineCodeRuns 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' } }];
NodeTypeJob
Pick signup fieldsEdit FieldsKeeps 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 lineCodeSame 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:

NodeTypeJob
To text file (purchases)Convert to FileOperation 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 purchasesRead/Write FilesOperation 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 FileSame operation as the purchases side, with the file name events-signups.csv.
Append signupsRead/Write FilesSame operation as the purchases side, writing to the signups file.
ReplyEdit FieldsReturns 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).

TRAPtwo 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

TRAPtwo 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).
TRAPanything 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).
TRAPthe 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.
TRAPConvert 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 }}.
TRAPthe 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.
TRAPthe 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

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.