n8n Filter node: clean data filtering (conditions, AND/OR, common traps)
Published 31 July 2026 · 6 min read
Almost no n8n workflow processes 100% of the items it receives: out of 50 rows in a Google Sheet, only the new ones matter; out of 200 fetched emails, only those from a specific domain; out of an API's orders, only those above a certain amount. The Filter node is the dedicated tool for this triage: it lets through the items that meet your conditions and silently discards the rest. Simple on the surface, it hides a few subtleties — AND/OR combinations, data types, case sensitivity, nested fields — and one classic trap that "stops" the workflow when nothing passes. This guide covers all of it.
The principle: one input, one output, conditions
The Filter node belongs to the transformation family, alongside Edit Fields (Set) or Split Out and Aggregate. Its behavior fits in one sentence: every incoming item is evaluated against one or more conditions; if it satisfies them, it goes through, otherwise it vanishes from the flow.
A condition has three parts:
- The value to test: most often an expression pointing at a field of the item, such as
{{ $json.status }}or{{ $json.amount }}; - The data type and comparison operator: the node groups operators by type —
- string: is equal to, contains, starts with, ends with, matches regex, is empty…;
- number: equal, greater than, smaller than, greater than or equal…;
- date: is after, is before, is equal to;
- boolean: is true, is false;
- array and object: contains, is empty, has a given length…;
- The comparison value: fixed (
"paid",100) or an expression itself.
To reach a field nested in the JSON, dot notation works everywhere: {{ $json.customer.address.city }} walks down the object, {{ $json.lines[0].amount }} grabs the first element of an array. If a level may be missing, prefer optional chaining ({{ $json.customer?.address?.city }}) to avoid an evaluation error — the fundamentals of this syntax are covered in our n8n expressions guide.
Combining several conditions: AND or OR
From the second condition on, the node asks how to combine them:
- AND: the item must satisfy every condition (order paid and amount > €100);
- OR: one is enough (status "urgent" or VIP customer).
The combination mode applies to all the node's conditions at once. If your logic mixes both — "(A and B) or C" — you have two options: chain two Filter nodes (two consecutive ANDs equal one global AND), or write the whole logic as a single boolean expression:
{{ ($json.status === "paid" && $json.amount > 100) || $json.vip === true }}
A single boolean "is true" condition on that expression replaces three conditions and their combination. It is often the right middle ground before reaching for a Code node.
Two settings deserve attention in the node's parameters: the option to ignore case (by default, "Paris" ≠ "paris") and the strictness of type validation, which decides whether "10" (string) can be compared to 10 (number) — more on that in the traps section.
Filter or IF: what's the difference?
Both nodes evaluate exactly the same conditions; the difference is topological:
- Filter: a single output. Non-matching items are dropped, full stop;
- IF: two outputs,
trueandfalse. Every item goes down one of the two branches, none is lost.
The decision rule is simple: if discarded items have no further use in the workflow, pick Filter — the canvas stays readable, with no dead branch. If both populations need processing (paid invoice → archive, unpaid → reminder), it's IF. Beyond two cases, the Switch node takes over: our guide to conditional routing with IF and Switch covers those scenarios.
Expression inside the Filter or a Code node?
The Filter with a boolean expression covers the vast majority of needs. The Code node only becomes relevant when:
- the logic depends on other items (comparing each item to the batch average, deduplicating against previous ones) — an expression only sees one item at a time through
$json; - the filter needs intermediate structures (building a Set of already-seen IDs, cross-referencing two lists);
- you want to log why each item was discarded.
In a Code node, the filter is a one-liner:
return items.filter(item => item.json.amount > 100 && item.json.status === "paid");
But as long as the condition can be phrased item by item, stay with the Filter node: it is visible on the canvas, editable without reading code, and its conditions document themselves.
Three concrete use cases
- Keeping only new rows: after reading a Google Sheet or an API, filter on
{{ $json.created_at }}"is after" the timestamp of the last run (stored in a Data Table or a file). For robust ID-based deduplication, the dedicated node does better: see our Remove Duplicates guide; - Filtering emails: after a Gmail or IMAP trigger, a condition like "sender ends with @important-client.com" or "subject contains invoice" removes the noise before any AI processing — every item filtered upstream is one API call saved;
- Excluding already-processed items: filter on a marker field (
{{ $json.processed }}"is false") that the workflow sets at the end of its run. This pattern prevents double sends when a workflow repeatedly scans the same source, and pairs well with Loop Over Items batching.
This triage work is not cosmetic: the reference study by Erhard Rahm and Hong Hai Do, "Data Cleaning: Problems and Current Approaches", published in 2000 in the IEEE Data Engineering Bulletin (see on Google Scholar), classified data quality problems (missing values, duplicates, format inconsistencies) and showed that detecting and removing them is a major part of any data integration pipeline — exactly the role Filter and its neighbors play in an n8n workflow.
The classic traps
- Zero items out = the rest of the workflow doesn't run. This is not a bug: n8n does not execute a node that receives nothing. If a downstream node must run even without results (a "nothing to report" message, a counter), enable Always Output Data in its settings — the node will then emit an empty item you must handle explicitly (test
{{ $json.isEmpty() }}or the absence of an expected field); - Comparing different types: APIs often return
"10"as a string where you compare against10as a number. Depending on the configured type validation, the condition fails or converts silently. The safe reflex: convert explicitly in the expression ({{ Number($json.amount) }}) or normalize upstream with a Set node; - Case sensitivity: "VIP" ≠ "vip" by default. Enable the ignore-case option or normalize with
.toLowerCase(); - Missing field vs empty field: an absent field,
nulland an empty string do not react the same way to "is empty" / "exists" operators. Pin real data and test all three cases — workflow debugging often starts there; - Filtering too late: place the Filter as early as possible in the workflow. Filtering 500 items after enriching them with AI means paying for 450 calls for nothing.
Key takeaways
The Filter node is your workflow's bouncer: one input, one output, and only matching items get in. Choose it over IF when discarded items serve no further purpose, combine conditions with AND/OR or a single boolean expression for mixed logic, and reserve the Code node for filters that need to look at several items at once. Watch for the three recurring traps — empty output halting downstream nodes (Always Output Data), cross-type comparisons, case sensitivity — and always filter as early as possible: every item dropped upstream saves time, quotas and API calls downstream.
FAQ
Frequently asked questions
What is the difference between the Filter node and the IF node in n8n?
The Filter node has a single output: items matching the conditions continue, the rest are simply dropped from the flow. The IF node has two outputs (true and false) and routes every item to one branch or the other. Use Filter when you no longer care about the discarded items, IF when both populations need different processing.
Why does my workflow stop after the Filter node?
If no item passes the conditions, the Filter produces no output and downstream nodes never execute: that is n8n's normal behavior. If a downstream node must run regardless (a daily report, a notification), enable "Always Output Data" in that node's settings, then handle the zero-item case explicitly.
How do I filter on a nested JSON field?
Use dot notation in the expression of the value to compare: {{ $json.customer.address.city }} reaches the city property nested inside address, itself inside customer. For an array element, add the index: {{ $json.lines[0].amount }}. If an intermediate level may be missing, protect the expression with optional chaining: {{ $json.customer?.address?.city }}.
Is the n8n Filter node case-sensitive?
Yes, by default string comparisons distinguish upper and lower case: "Paris" does not equal "paris". The node offers an option to ignore case in its settings; you can also normalize upstream with an expression like {{ $json.city.toLowerCase() }}.
Bundle FlowKit Complet
€269