Skip to Content

Splitting a workflow in two, and putting it back together

the fork in the road
n8n Guide · Part 11

Splitting a workflow in two, and putting it back together

Everything so far has been a straight line: data arrives, each node hands it to the next. Real automations are not like that. An order over 1,000 goes to the manager and the rest go on a list. Five hundred rows have to reach an API that allows five calls a second. Two branches finish separately and you want one notification at the end. This article covers the nodes that split a path, and the nodes that join it back up.

2 outputs
What an IF node always has, true and false
4 modes
The ways a Merge node can join two paths
0 API calls
What flow control costs you
Why a workflow forks

Passing data along is not the same as deciding what to do with it

By now you can write {{ $json.amount }} and push a value from one node into the next. That is passing something along untouched. Almost every automation anyone actually runs has a fork in it: notify the manager only when the amount is over 1,000; email the thank-you note only to VIP customers; store the form only if the consent box was ticked; make critical alerts ring a phone while info only writes a log line.

Read the data, then pick a path. Programmers write that as if / else or switch / case. In n8n you write no code: you drop in an IF node or a Switch node and draw the downstream connections by hand. Data comes in, the node decides, and the item leaves through the top or the bottom (IF) or through a numbered output (Switch).

In plain terms

It is the sorting table in a post room. Every envelope goes past the same person, who reads one thing on it — the postcode, the department, whether it is marked urgent — and drops it into one of the trays. That person is not doing the work. That person is deciding whose desk the work lands on.

Branching is free. IF and Switch are both n8n Core nodes. They call no outside API and use none of your SaaS quota. They do flow control inside the workflow, and running them costs practically nothing.

Splitting is only half of it. The other half arrives once you have built a few workflows: you split with IF and cannot work out how to join the paths back; you feed 500 items to an API that allows 5 a second and collect 429 Too Many Requests from the sixth onward; an API returns { total: 5, items: [...] }, which counts as 1 item, so the Slack node downstream sends once instead of five times; two APIs each return half a record and you need them stitched on the same customer_id. Those are the jobs of Merge, Loop Over Items, Split Out and Aggregate.

A note on names. This article uses the current official ones. The old Split In Batches is now Loop Over Items — the editor shows it as “Loop Over Items (Split in Batches)” — and the old Item Lists has been broken into Split Out, Aggregate, Sort, Limit, Remove Duplicates and Summarize. Search for the new names. Older exported workflows still run: the splitInBatches type stays compatible.
IF or Switch

Two paths, or several

Both nodes decide, then route. Picking the wrong one breaks nothing, but the right one keeps the canvas readable.

CharacteristicIF nodeSwitch node
Outputs Always two: true (top), false (bottom) As many as you define. Rules mode has no documented hard limit; 3–10 is common in practice. In Expression mode Number of Outputs decides
The question it answers One yes/no — is the amount over 1000? One field with several values — is tier gold, silver or bronze?
Mental model A program's if / else A program's switch / case. Like an elevator: press a floor, it takes you there
How to choose Only two paths: yes or no Three paths or more, or an obvious category — tier, status, source
flowchart LR
  subgraph SW["Switch · one field, several values"]
    direction LR
    S0["An item arrives"] --> S1{"read tier"}
    S1 -->|"gold"| S2["output 0"]
    S1 -->|"silver"| S3["output 1"]
    S1 -->|"bronze"| S4["output 2"]
    S1 -->|"nothing matched"| S5["fallback output"]
  end
  subgraph IFN["IF · one yes-or-no question"]
    direction LR
    I0["An item arrives"] --> I1{"amount over 1000"}
    I1 -->|"true"| I2["Notify the manager"]
    I1 -->|"false"| I3["Daily log"]
  end
Two dots, or as many as you needThe IF lane is the upper one: an item comes in on the left, one question is asked, and there are exactly two ways out — there always will be. The Switch lane underneath asks about one field and grows one more output for every rule you add, plus the fallback output at the bottom once you turn it on.
“Three categories or more” is not a hard rule. You can stack three IF nodes — the first tests gold, the one below it silver — but that turns the canvas into a staircase. When one field has several possible values, reach for Switch.
Build your first IF

An order over 1,000 goes one way, everything else the other

Assume your upstream node — a Webhook, Google Sheets, Airtable, whichever you have — hands the order down as JSON in this shape:

{ "amount": 1500, "customer": "Example Stationery Store" }
  1. Step 1

    Add an IF node

    Click the + icon to the right of the upstream node, or press Tab on an empty spot, to open the nodes panel. Type if and click If, under the Flow category. It drops onto the canvas already connected upstream.

  2. Step 2

    Set the first condition

    Double-click the node. A Conditions block appears with one empty comparison row, three fields left to right:

    Value 1Click the field to switch to Expression mode and write {{ $json.amount }}.
    OperatorPick the type Number on the left first, then is greater than on the right — the official name in the v2 IF node, not larger.
    Value 2Enter 1000. A plain number, no quotes.
  3. Step 3

    Wire the two outputs that appear

    The node now shows true on top in green and false below in red. Drag from true into your “notify the manager” node — a Slack node posting to a manager channel — and from false into the “daily log” node, a Google Sheets Append Row for example.

  4. Step 4

    Save, then test the true path

    Press Ctrl+S, then click Execute Workflow at the top right. The IF node marks the output it took with a green border. The amount is 1500, over 1000, so it should take true — and the Slack node should light up too.

  5. Step 5

    Test the false path as well

    Change the upstream mock data to amount: 800 and run again. Only the Google Sheets path should light up. An IF is only verified once you have tested both sides.

Four data types, and the mistake everyone makes once

The Operator dropdown has two levels: pick the data type on the left, and the comparisons for that type appear on the right. Get the type wrong and the condition reads perfectly but comes out false every run.

TypeOperatorsValue 2, and an example
String exists / does not exist / is empty / is not empty / is equal to / is not equal to / contains / does not contain / starts with / does not start with / ends with / does not end with / matches regex / does not match regex A string; an expression is allowed. {{ $json.status }} · is equal to · paid
Number exists / does not exist / is empty / is not empty / is equal to / is not equal to / is greater than / is less than / is greater than or equal to / is less than or equal to A plain number, no quotes. {{ $json.amount }} · is greater than · 1000
Boolean exists / does not exist / is empty / is not empty / is true / is false / is equal to / is not equal to Nothing at all for is true and is false; for is equal to, true or false. {{ $json.consent }} · is true
Date & Time exists / does not exist / is empty / is not empty / is equal to / is not equal to / is after / is before / is after or equal to / is before or equal to An ISO date string or an expression such as {{ $now }}. {{ $json.createdAt }} · is after · 2026-01-01

Two more come up less often: Array (contains, length equal to, length greater than) and Object (only exists, does not exist, is empty, is not empty). Note that Date & Time has no is between — to test “between two times” write two conditions, is after A and is before B, combined with AND.

In plain terms

A price tag reading 1500 and the number 1500 are not the same thing to a computer. One is a piece of paper with ink on it; the other is a quantity you can compare. Ask a machine whether the piece of paper is bigger than a thousand and it shrugs, every time, forever.

The number-one gotcha. The data is plainly the string "1500", you picked Number → is greater than → 1000, and it is false on every run. The string 1500 is not the number 1500, and n8n does not convert it for you. Fix it in Value 1 with {{ Number($json.amount) }}, or convert upstream with an Edit Fields (Set) node. Whenever a condition should match and does not, this is nearly always why.

Combining conditions with AND and OR

Add a second condition row and an AND / OR dropdown appears between the two. The interface calls it exactly that; there is no Combinator label to hunt for. AND is worded “Keep data when it meets all conditions”. OR is worded “Keep data when it meets any of the conditions”.

AND“A big amount is not enough, the money has to have arrived.” Condition 1 {{ $json.amount }} Number is greater than 1000; condition 2 {{ $json.status }} String equals paid. With status pending, even 5,000 goes false.
OR“VIP customer, or a large order — either deserves attention.” Condition 1 {{ $json.tier }} String equals vip; condition 2 {{ $json.amount }} Number is greater than 5000.
Mixed logic needs a workaround. “A AND (B OR C)” is out of reach for one IF node, because the combinator applies to the whole node. Either chain two IF nodes — the first tests A, its true output feeds a second that tests B OR C — or put the whole test in Value 1 as {{ $json.a && ($json.b || $json.c) }} and set Operator to Boolean is true. The second is tidier, but you need a little JavaScript.
The Switch node

Rules mode, Expression mode, and what happens to the leftovers

Open a Switch node and the first thing it asks is Mode: Rules or Expression. Rules works like a stack of IF nodes — every rule has its own condition, using the operator system you just learned, and an item leaves through the output belonging to whichever rule matched. Click Add Routing Rule and you get one more output. Three customer tiers, with { "tier": "gold", ... } arriving from upstream:

Rule 0{{ $json.tier }} String equals gold → output 0, the thank-you email and a note to sales
Rule 1{{ $json.tier }} String equals silver → output 1, the discount voucher
Rule 2{{ $json.tier }} String equals bronze → output 2, the standard welcome email

Below the rules sits Fallback Output, with three official options: None (the default — an unmatched item is dropped), Extra Output (one more output catching unmatched items, the common choice) and Output 0 (unmatched items leave with the first rule's). When you want an “unclassified” path, pick Extra Output.

flowchart TD
  A["One item enters the Switch"] --> B{"Rule 0 matches?"}
  B -->|"yes"| B1["Leaves through output 0.
The rules below are skipped"] B -->|"no"| C{"Rule 1 matches?"} C -->|"yes"| C1["Leaves through output 1"] C -->|"no"| D{"Rule 2 matches?"} D -->|"yes"| D1["Leaves through output 2"] D -->|"no"| E{"Fallback Output setting"} E -->|"None, the default"| E1["The item is dropped.
Nothing errors"] E -->|"Extra Output"| E2["Leaves through
the extra output"] E -->|"Output 0"| E3["Leaves with the
first rule's items"]
Rules are read top to bottom, first match winsThat last question is the one people forget. With Fallback Output on None, an item nothing matched does not fail and does not warn. It just stops existing.

A Send data to all matching outputs toggle sits under Options, off by default; with it on, an item matching two rules is sent down both, so items get duplicated across branches. Because of the top-to-bottom order, put the strictest rule at the top. To stop caring about order entirely, write rules that exclude each other: amount >= 100 AND amount < 1000, then amount >= 1000 AND amount < 5000, then amount >= 5000 — each range built from two conditions plus AND, since Number has no is between.

Expression mode instead takes one expression returning the output number, counting from 0. The same three tiers:

{{ { "gold": 0, "silver": 1, "bronze": 2 }[$json.tier] }}

Read it as a lookup table, and set Number of Outputs first — 3 here — so the node has that many lines to connect. Neither mode is the advanced one: Rules is friendlier because you read the logic off the canvas, Expression saves setup time when there are many categories and the mapping is one-to-one. Both give the same result.

SituationNode and conditionWiring
Store the form only if consent was ticked, otherwise ask for it again IF: {{ $json.consent }} Boolean is true true → Google Sheets Append; false → Gmail Send Message
Tiered notifications: under 1000 the assistant, 1000 to 5000 the team lead, over 5000 the manager Switch (Rules): rule 0 amount under 1000; rule 1 two conditions with AND, at least 1000 and at most 5000; rule 2 over 5000 One Slack node each, posting to a different channel
Route incoming mail by the sender's domain Switch (Rules): {{ $json.from.split('@')[1] }}, one String equals rule per domain Each to a Gmail Add Label or Move Message
System alerts by severity Switch (Expression): {{ { critical: 0, warning: 1, info: 2 }[$json.level] }} output 0 → HTTP Request to the phone API; output 1 → Slack; output 2 → HTTP Request that writes the log
A webhook receives a payload and each event_type needs a different path Switch (Rules): one event_type string per rule Each rule into its own chain of handling nodes. Turn the fallback on and wire an “unknown event” alert to it, or a payload you have never seen before vanishes
An unconnected output is a valid answer. If an IF node's false output has nothing to do, leave it unconnected. Items that go that way stop there, with no error and no effect on the true path.
Nest two levels at most. A branch can branch again, and two shallow levels — an IF for VIP, then a Switch by country on the VIP path — stay readable. Pull it into a sub-workflow once you hit three levels, or the same decision appears in several workflows, or one branch has more than five nodes under it. Remember too that every level splits the item count again: if 100 records come in, each branch ends with a different number, and downstream nodes have to survive “no items at all”. That is the usual answer to “there were 100 records upstream and Slack only sent 3”.
Joining paths with Merge

Four modes, and three more choices inside one of them

The Merge node always needs two inputs — you see two connector dots when you draw a connection to it. Its Mode dropdown has four options. This is the structure n8n moved to after 2024, and it is not the “three modes” older tutorials describe.

Do not simply point both branches at the same node. You can drag the true output and the false output into one downstream node, and n8n will merge the items from both and hand them over — which is why almost everyone tries this first. Be careful: the order is not guaranteed and the fields may not line up. Using the Merge node's Append or Combine mode to state exactly how the two sides join is safer.
ModeWhat it doesNotes
AppendKeep data from all inputs: stacks both sides one after the other3 items plus 2 items gives 5. Like pasting rows under rows in a spreadsheet
CombineCombine data from two inputs: joins the fields side by side, then you pick a Combine By strategyThe most-used mode. It replaces the old Combine by Position / by Key / Multiplex
SQL QueryWrite SELECT ... FROM input1 JOIN input2 ... and define the rule yourselfRun on alasql underneath. Use it for multi-field joins, CASE WHEN, a UNION with a WHERE, subqueries — not for a plain append
Choose BranchKeeps only one side: of the two branches, carry this one on downstreamNot a merge, a choice. It replaces the old trick of rigging this up with two IF nodes

Under Combine sit three strategies. Matching Fields pairs items on a key value, the same as a SQL JOIN — customer data on id against order data on customer_id. Position aligns by position in the array, so [{a:1},{a:2},{a:3}] plus [{b:x},{b:y},{b:z}] gives [{a:1,b:x},{a:2,b:y},{a:3,b:z}]. All Possible Combinations is the Cartesian product: 3 items and 2 items give 6.

With Matching Fields selected, Output Type decides the kind of join, and all five are there. Keep Matches outputs only items matched on both sides, an inner join. Keep Non-Matches outputs only the unmatched, which is how you answer “which customers have never ordered”. Keep Everything keeps both sides and fills unmatched fields with null, a full outer join. Enrich Input 1 lets Input 1 lead and pastes on the matching fields from Input 2, a left join; Enrich Input 2 is the mirror image.

If you know SQL, you already know these. Combine → Matching Fields is a JOIN, Append is UNION ALL, Split Out is UNNEST, Aggregate is GROUP BY collected into an array, and Summarize is GROUP BY with aggregate functions. The three smaller nodes map just as directly: Sort is ORDER BY, Limit is LIMIT, and Remove Duplicates is DISTINCT. Only the names change.

One behavior catches almost everybody once. A Merge node only runs when data has arrived at both inputs. Rejoining after an IF split, if one path is meant to do nothing, wire a No Operation, do nothing node into Merge anyway — or Merge waits forever, quietly, with no error to explain itself.

In plain terms

It is arranging to meet someone at the station entrance. You are there, they went home instead, and nobody rings to cancel. You are not stuck because anything broke. You are stuck because the arrangement only completes when both people show up, and one of them was never coming.

flowchart TD
  A["Upstream node"] --> B{"IF"}
  B -->|"true"| C["Edit Fields (Set) A"]
  B -->|"false"| N["No Operation,
do nothing"] C --> M["Merge
Mode = Append"] N --> M M --> S["Slack, written once"]
Split, do different work, write the downstream onceThe true branch runs down the left into an Edit Fields (Set) node. The false branch runs down the right into No Operation, do nothing, which changes no data and is there only to carry a connection. Merge has two arrows arriving and no third one, so it fires, and Slack is written once instead of twice.

Two last details. Ordering has a rule per mode: Append gives all of Input 1 then all of Input 2, Position keeps the same index aligned, and Matching Fields orders by the order the keys appear in Input 1 — put a Sort node before or after the Merge if you need a particular order. And the node has connectors for two inputs only: to merge three or more, chain two Merge nodes, A and B into the first, its output plus C into the second.

Many items at once

Batching, flattening and rolling back up

NodeIn and outClassic use
Loop Over Items
(formerly Split In Batches)
One input with many items; two outputs, loop for this round's items and done for when everything has finishedAPI rate limits, memory, a progress message per batch
Split Out
(formerly Item Lists · Split Out Items)
One item in, many out — the array flattenedAn API returning { items: [...] }, a Gmail attachment array
Aggregate
(formerly Item Lists · Aggregate Items)
Many items in, one outReport roll-ups, one daily summary instead of many alerts
Sort / Limit / Remove Duplicates / SummarizeMany in; many or one outThe other old Item Lists operations, each its own node now

The Airtable API allows at most 5 requests a second and your sheet has 500 rows to write. Wire it straight through and after the fifth item you start collecting 429s, with the remaining 495 failing. Loop Over Items is the fix, and its unusual part is that the node forms the loop itself: the nodes on the loop side have to draw a connection back to it, and that back-connection is what makes it a loop.

flowchart TD
  A["Google Sheets
500 items"] --> B["Loop Over Items
Batch Size = 5"] B -->|"loop"| C["Airtable Create"] C --> D["Wait, 1 second"] D -->|"the connection back is
what makes this a loop"| B B -->|"done, taken once
everything has run"| E["Slack: 500 records processed"]
Five at a time, with a pause between roundsRemove the Wait node and you still have batches but no gap between them — the requests go out almost at once and the rate limit is hit anyway.

Batch Size sets how many items each round sends to the loop output: 5 for Airtable, 10–50 for a typical API. Options → Reset decides whether the counter resets when new items arrive — leave it off for a one-shot workflow, turn it on only when the workflow is called continuously. The done output is taken once, after everything has run, and feeds an “all finished” notification.

The Wait node is what makes this work. Five calls a second allowed, Batch Size 5, Wait 1 second, and you run steadily just under the limit. Loop Over Items has no progress bar of its own, but every round passes through the same nodes — so add a Slack node on the loop branch to report “batch N done, M batches left”.

Split Out and Aggregate

A Gmail message whose payload holds an attachments array with five attachments is 1 item, and an upload node downstream would run once. Split Out is what turns a JSON array into n8n items — the answer to the point made earlier in this series, that n8n items and a JSON field called items are two different things. Fields To Split Out names the field holding the array, comma-separated for several. Include chooses whether each item that comes out carries the original's other fields, such as the mail subject: No Other Fields / All Other Fields / Selected Other Fields. Under Options, Destination Field Name names the key each payload sits under, or flattens it when left empty, and Disable Dot Notation (off by default) is for field names that themselves contain a ..

Aggregate is the opposite: five processed items collapsed into one summary. Individual Fields aggregates only the fields you name, with Rename Field and Merge Lists to flatten nested arrays. All Item Data (Into a Single List) packs every item's whole json into one array, with Include and Exclude lists.

In plain terms

A delivery arrives as one box with five parcels inside, so the courier's system scans it once. Split Out is opening the box and putting five parcels on the counter, each scanned in its own right. Aggregate is packing them back into one box at the end of the day so you send one note instead of five.

Worked example: one Slack reminder per Google Sheet row

Start with the case that needs none of this section's nodes, because it shows you what “already many items” looks like. A Google Sheets read turns every row into an item on its own, so the work is done before you get there.

  1. Step 1

    Add a Manual Trigger

    Create a workflow and pick Trigger manually as the first node. Pressing Execute Workflow then runs it once, on demand.

  2. Step 2

    Add a Google Sheets node

    Wire on a Google Sheets node and set Operation to Get Rows in Sheet. Connect the credentials — Part 10 of this series covers that — then pick your spreadsheet and sheet. Say the spreadsheet is called “Customer list”, has 10 rows, and its fields are name, email and due_date.

  3. Step 3

    Execute step and confirm 10 items arrive

    Press Execute step on the Google Sheets node. The top of the output panel on the right shows 10 items — check the count is right before you go on. Switch to the Table view to read each row.

  4. Step 4

    Add a Slack node

    Wire on a Slack node, set Operation to Send Message and pick a channel — #billing-reminder, for example. Switch the Text field to Expression mode and enter:

    Statement reminder for {{ $json.name }}. Due {{ $json.due_date }} — please take care of it.
  5. Step 5

    Execute the whole workflow once

    Press Execute Workflow at the top right. The Slack node's output panel shows 10 items — 10 messages sent, one per row. Open the channel and confirm 10 actually arrived.

  6. Step 6

    Save it

    Save at the top right. From then on, pressing Execute Workflow sends another round. To send it automatically on the 1st of each month, swap the Manual Trigger for a Schedule Trigger, which Part 7 of this series covers.

Worked example: split the array an API returns and act on each entry

Try it end to end. Add a Manual trigger, then an HTTP Request node with Method GET and URL https://jsonplaceholder.typicode.com/users. Press Execute step and read the top of the output panel: 1 item, not 10, because the whole response is one array. Wire on a Split Out node and put the path of that array in Fields To Split Out — if HTTP Request puts it in data or at the root, use the path that matches, read off the output panel. Execute step again and the panel shows 10 items. A Slack node with Send Message and Text set to Welcome {{ $json.name }} ({{ $json.email }}) now sends 10 messages instead of one.

To roll it back up, add an Aggregate node with Aggregate set to Individual Fields and Field To Aggregate set to name, renamed to names. The output is back to 1 item holding an array of 10 names, and a Gmail node can send Processed these 10 users today: {{ $json.names.join(', ') }} — one summary instead of ten alerts.

Fields To Split Out has to name the array itself, not a field inside it. Getting that wrong fails quietly: the output comes out as 1 item, the same as the input, so the node looks like it did nothing. Switch to the Schema view on the upstream node, find the field whose type is array, and paste its full path in — data.results, for example.
Two habits worth keeping. Not every “many items” problem needs these nodes — a Google Sheets read of a 10-row sheet already gives you 10 items, and “naturally many items” is a different situation from “many items packed inside one array”. And while you are developing, wire a Limit node after the data source with Max Items set to 10, then deactivate or delete it before you go live. That is what keeps a test from really sending 500 emails.

Four combinations you will reach for most weeks

Knowing one node is only the start. Almost every real workflow is two or three of them standing next to each other.

CombinationWhat it solvesStructure
IF + MergeEach branch does its own thing, then they rejoin into one path that carries on downstreamIF → true branch (Set A) and false branch (Set B) → Merge (Append) → Slack
Loop Over Items + WaitBatching plus a pause, to stay under a rate limitLoop Over Items → HTTP Request → Wait 1s → back to Loop Over Items
Split Out + processing + AggregateOne big array from an API, something done to each entry, then the lot collapsed into one summaryHTTP Request → Split Out → processing nodes → Aggregate → Email
Merge, Combine · Matching FieldsTwo data sources joined on a key into one complete recordHTTP A and HTTP B both into Merge (Mode = Combine, Combine By = Matching Fields, id against customer_id) → downstream
Build each one on the practice workflow first. Every one of these is worth trying on the practice workflow from Part 6 of this series before you point it at real business work. Get it running there, on data nobody is waiting for, and then move it across.
When a branch misbehaves

A branch that will not fire, goes the wrong way, or refuses to split

Almost all of it comes down to four questions, asked in this order.

flowchart TD
  A["A branch did not do
what you expected"] --> B{"Did any item reach
the IF or Switch node?"} B -->|"no"| B1["The problem is upstream.
Read the item count on
the node before it"] B -->|"yes"| C{"Do the output counts add up
to the number that went in?"} C -->|"no"| C1["Items were dropped.
On a Switch that is
Fallback Output left on None"] C -->|"yes"| D{"Is the value a string
where you picked Number?"} D -->|"yes"| D1["Wrap Value 1 in Number,
or convert it upstream
with a Set node"] D -->|"no"| E{"Is a Merge node
downstream sitting idle?"} E -->|"yes"| E1["One input never received data.
Wire No Operation, do nothing
onto the empty branch"] E -->|"no"| F["Read the condition
row by row. Case and
whitespace count"]
Four questions, in orderMost of these are answered by reading item counts rather than by changing settings. Start editing the condition first and you lose the evidence that would have told you which one it was.
SymptomLikely causeHow to fix it
“Both branches ran” It cannot happen. IF is exclusive: each item goes true or false, never both. Both sides light up because several items came in, some going each way Read the item count on each output. The two should add up to the upstream total
“It should match, yet it always goes false” The data type does not match the operator type — the string "1500" against Number is greater than {{ Number($json.amount) }} or {{ String($json.status) }} in Value 1, or convert upstream with a Set node
“Some items disappear in the Switch” They matched no rule and Fallback Output was never turned on Change Fallback Output from None to Extra Output, then wire the new output to an “unclassified” Slack alert or sheet row
“contains does not match, though the text is plainly there” contains is case-sensitive, or there is whitespace around the field {{ $json.text.toLowerCase().trim() }} in Value 1, and lowercase in Value 2 as well
“A date is before or after test always goes false” Value 1 holds a date as a string, not a Date object. n8n converts it sometimes and sometimes not {{ DateTime.fromISO($json.createdAt) }} converts it explicitly into a Luxon DateTime
“Expression mode says the output number is invalid” The expression returned a number outside the Number of Outputs range — 3 outputs set, 5 returned Give it a floor: {{ ({a:0,b:1,c:2}[$json.type]) ?? 0 }}, so anything unmatched goes to 0
“The IF node opens with no Conditions block” A version difference, or the node did not finish loading Delete the node and add it again, or close the canvas tab and reopen the workflow
“Split Out runs and the output is identical” Nine times out of ten Fields To Split Out is wrong: misspelled, or a field inside the array rather than the array, or not an array at all but an object Read the full path off the Schema view of the upstream node and paste it in
“Combine · Position loses half the data” Position aligns by index, so 3 items against 5 gives 3 — the shorter side wins and the last 2 are dropped Use Combine By Matching Fields with Output Type Keep Everything, or Append to stack them
“The Merge node sits there and never runs” Merge runs only when both inputs have data. If every condition came out false, nothing reaches the true branch and Merge waits forever Put a No Operation, do nothing node on the empty branch, or use the fallback so something reaches both inputs
“Loop Over Items never stops, or runs one round and gives up” Either the loop-branch nodes are not wired back to the node — one round runs, the rest is dropped, nothing errors — or Options → Reset is on while new items keep arriving upstream Draw the connection back to Loop Over Items and check you did not wire it to done by mistake. Leave Reset off unless you are building a long-running service
“The Wait is not the pause I expected” Wait means “this item waits N seconds before moving on”. With Batch Size 5, all five reach it at nearly the same moment and each waits its own N — not one shared pause For 5 seconds between batches, keep Batch Size 5 and Wait 5, then read the timestamps of the requests that actually went out

Questions people ask next

After an IF I want both paths to carry on with the original flow. How do I write that?
If both paths do exactly the same thing, you do not need an IF at all — connect the upstream node straight to the downstream one. An IF exists precisely because the two paths do different things. If what you actually want is “both paths do the same work, I only want a label telling them apart”, drop the IF and add a field with an Edit Fields (Set) node instead: tag set to vip or normal. Then let the downstream nodes work from that field. The data flow stays in one piece and you keep the classification. When the two paths really do different work and you want them back on one line afterwards, that is what the Merge node is for.
Do lots of IF nodes cost anything — speed, or quota?
Barely any speed: IF and Switch are pure computation, no API and no I/O, usually milliseconds or less. Fifty of them are still faster than one extra call to the Gmail API. On quota, keep two levels apart. A workflow-level execution is one trigger running to the end; it counts as one however many nodes are inside, and it is what n8n Cloud bills for. A node-level execution is each node run counted separately, which is what the execution detail page shows for debugging. Woow n8n is self-hosted, and neither level is billed. An IF counts only at the node level and spends no workflow-level quota, and it calls no SaaS API at all — unlike Gmail Send, which uses one Gmail quota unit. Use as many as you need, and do not cram the logic into a Code node to keep the node count down.
What is the maximum number of Switch outputs? Can an IF have three?
The documentation sets no hard limit on Switch rules — keep clicking Add Routing Rule. In practice 3–10 is very common, which is 4–11 outputs once you count the fallback. Expression mode has no documented ceiling on Number of Outputs either, but past 10 it is usually time to consider a sub-workflow. An IF node always has exactly 2 outputs, true and false, by definition. For three paths use a Switch; do not force it with an IF inside an IF.
The condition is too complex to build in the interface. What do I do?
Write the whole test as one JavaScript expression in Value 1 that returns true or false, then set Operator to Boolean is true: {{ $json.amount > 1000 && ($json.tier === "vip" || $json.paid) && !$json.blocked }} fits “A AND (B OR C) AND NOT D” on one line. Readable is still better, so when it really is complicated, move it into a Code node that returns { shouldNotify: true } and let the IF test {{ $json.shouldNotify }}.
Can an IF condition read data from two nodes back?
Yes, with the cross-node syntax: {{ $node['Node Name'].json.field }}, for example {{ $node['Webhook'].json.body.customer_id }}. This is common — the current node holds a lookup result, but you want to decide on the original payload the webhook was triggered with. Node names are case-sensitive and spaces inside them have to be kept.
Can the number of branches be decided at run time?
No. Outputs are fixed at design time and do not grow while the workflow runs. If your categories are genuinely dynamic, group the data by that field with Loop Over Items or the Item Lists successors and run the same downstream logic on each group, possibly inside a sub-workflow. That is more flexible than growing outputs at run time, and much closer to how data processing is meant to work.
How large can Batch Size be on Loop Over Items?
There is no hard technical ceiling. Start at 10–50 and stay under 500. Too small — 1, say — and the node runs hundreds of rounds, slowing everything down; too large — 5000 — and you lose the point of batching while eating memory. Sit right up against the downstream API's rate limit, or the per-batch limit of its batch API: the Airtable batch endpoint takes at most 10 records at a time, so set Batch Size to 10. Note that items inside one execution are processed in sequence — a node handling 100 items runs 100 times, not 100 threads at once. For real parallelism you self-host with several workers (EXECUTIONS_MODE=queue) so several executions run at the same time.
What if Loop Over Items fails halfway? Does the work already done roll back?
Nothing rolls back — n8n has no concept of a transaction. Whatever was written into Airtable or Slack stays written, and a failure on batch N only affects what comes after it. Two things help: add error handling on the loop branch so one failed item is logged instead of stopping the run, which is the next article's subject; and deduplicate before you process, so a rerun does not create duplicate data.
What decides a match in Combine · Matching Fields? Is it case-sensitive?
n8n compares with strict equality: same type and same value. Strings are case-sensitive, so "Alice" and "alice" do not match, and types matter, so the string "123" and the number 123 do not either. When the types disagree, line them up with an Edit Fields (Set) node first, or write a CAST in SQL Query mode.
Split Out or the Code node for splitting an array?
For a plain “expand the array in this field into several items”, Split Out needs no code. When the split itself needs conditions, type conversion, filtering or computed fields, or the array has an odd shape such as arrays inside arrays, use the Code node. Do not write code when you do not have to; a built-in node comes first.
How do I use the array Aggregate produced in an expression?
Like any other array. If Aggregate collected name into names: {{ $json.names.join(', ') }} joins them with a comma and space, {{ $json.names.length }} counts them, and {{ $json.names.filter(n => n.startsWith('W')) }} filters.
Next

Where to go from here

keep going

A workflow that branches can fail in more than one place.

Part 12 is the other half of flow control: what happens when a node throws, how to stop one bad item killing a run of 500, and how to make a failure tell you about itself instead of going quiet.

Open the full guide

Part 11 of the Woow n8n Onboarding Guide series on the Apporo blog.

Adapted from the Woow n8n Onboarding Guide, produced by WoowTech and released under CC BY 4.0. This adaptation is published by Apporo under the same licence.

Light · Air · Water · Control · apporo

in n8n
Permission to act as you, and a value to act on