In the previous post, I described nodes as "the building blocks" of a workflow. That's true, but it's a bit like calling a car engine "a metal box." Once you've built your first couple of workflows, the real question stops being what is a node and starts being how does a node actually work?
This post answers that. We'll look at what every node shares, how data moves between them, the different categories of nodes, and the details that separate someone who copies tutorials from someone who can build a workflow from scratch.
Every Node Has the Same Anatomy
No matter how different two nodes look in the editor, they all share the same four parts.
Input and output: Most nodes take data in, do something, and pass data out. Trigger nodes are the exception—they have no input, because they're the ones starting the flow. Some nodes have multiple inputs or outputs, like the If node, which routes data down one of two paths depending on a condition.
Parameters: These are the settings you fill in on the node's panel—which Slack channel, which HTTP method, which spreadsheet ID. Parameters are what make a generic node specific to your use case.
Credentials: Any node that talks to an external service needs to authenticate. n8n stores credentials separately from the node itself, so you can reuse the same Gmail connection across ten different nodes without re-entering anything.
Settings: Tucked behind a separate tab, these control behavior rather than purpose: whether the node should keep running if it fails, how many times to retry, whether to execute once per item or once total. Most people ignore this tab for months. Don't—it's where a lot of real-world reliability lives.
How Data Moves: Items and JSON
Here's the single most important concept in n8n, and the one beginners skip.
Data in n8n isn't a single blob—it's a list of items. Each item is a small JSON object that looks like this:
When a node outputs ten rows from a database, it outputs ten items. When the next node runs, it doesn't run once—it runs once per item. This is why an HTTP Request node pointed at an API can process a list of 500 URLs without you writing a loop.
{
"json": {
"name": "Ada Lovelace",
"email": "ada@example.com",
"signupDate": "2026-03-14"
}
}
Understanding this explains a lot of confusing behavior. If your Slack node sends five messages when you expected one, it's because five items arrived. If a node seems to "do nothing," it's often because it received zero items.
There's also a special idea called binary data—files, images, attachments. Those sit alongside the JSON in an item's binary property rather than inside it.
Expressions: Referencing Data From Earlier Nodes
Parameters aren't just static text. You can make them dynamic using expressions, which are snippets wrapped in double curly braces:
{{ $json.email }}
This pulls the email field from the current item. A few of the most useful built-in variables:
- $json — the current item's JSON data
- $node["HTTP Request"].json — data from a specific earlier node
- $now — the current timestamp
- $item(0).$json — the first item, regardless of which item you're currently on
The expression editor has a live preview, which makes this much easier to learn by tinkering than by reading. The trick is to reference data by its structure, not by guessing—open the input panel of the node you want and see what the fields are actually called.
The Node Categories
Now that the anatomy is clear, the categories make more sense.
Trigger nodes start a workflow. Schedule Trigger fires on a timer, Webhook Trigger fires when an external service calls your URL, and app triggers (Gmail, Slack, Notion) fire on events inside those services.
Action nodes do work in an external app—create a row, send a message, update a record. These are the hundreds of pre-built integrations you see in the node panel.
Core nodes are the generic tools that don't belong to any one service. This is where a lot of power lives:
- Code — write JavaScript or Python to transform data however you like
- HTTP Request — call any API that doesn't have a dedicated node
- If and Switch — branch based on conditions
- Merge — combine data from multiple branches
- Set (Edit Fields) — add, rename, or remove fields
- Split In Batches — process items in chunks, essential for rate-limited APIs
- Wait — pause the workflow
- Filter — drop items that don't match a condition
If you only ever master one category, make it the core nodes. They're the difference between being limited by what integrations exist and being able to build anything.
The Code Node, Briefly
The Code node deserves a note because it's the escape hatch. When no combination of built-in nodes does what you need, you drop in a few lines of JavaScript:
javascript
return items.map(item => {
return {
json: {
...item.json,
fullName: `${item.json.firstName} ${item.json.lastName}`
}
};
});
Two modes matter here. Run Once for All Items gives you the entire array at once, which is best for sorting, aggregating, or deduplicating. Run Once for Each Item runs your code per item, which is cleaner for simple transformations. Choosing the right mode avoids a lot of awkward looping.
That said—if you can solve something with core nodes, prefer that. Visual workflows are easier to debug six months later than a Code node full of logic nobody remembers writing.
Handling Errors at the Node Level
A workflow that works in testing but breaks in production usually breaks because of an unhandled node failure. Every node has settings for this:
Retry On Fail re-attempts the node a set number of times with a delay. This is the fix for flaky APIs and network hiccups.
Continue On Fail lets the workflow keep going even when the node errors, passing the error along as data so later nodes can handle it.
Always Output Data ensures the node emits at least one item even when it returns nothing—useful when you don't want the chain to stop silently.
You can also set a global error workflow that runs whenever any workflow fails, which is the cleanest way to get alerted. We'll go deeper on error handling in a future post, but knowing these three settings exist puts you ahead of most users.
Pinning, Testing, and Not Re-Calling APIs
One of n8n's most underrated features is pinning. When you pin a node's output, n8n freezes that data and reuses it on every subsequent test run—it doesn't call the API again.
This matters enormously during development. If your workflow sends three emails before the Slack step fails, you don't want to resend those emails every time you debug Slack. Pin the email node, and you can iterate on everything downstream without side effects. Just remember to unpin before you activate the workflow, or you'll ship stale data.
Putting It Together
If the first post was about connecting nodes, this one is about understanding them. The mental model that ties it together:
A node receives a list of items, runs once per item, transforms them, and passes a new list downstream—all configured through parameters, made dynamic with expressions, and made reliable with settings.
Once that clicks, n8n stops feeling like a drag-and-drop toy and starts feeling like a programming environment with a visual front end. That's exactly what it is.
Where to Go Next
The natural follow-ups are sub-workflows (reusing logic across multiple flows), error workflows (catching failures globally), and the Split In Batches / rate-limiting pattern (surviving APIs that don't like being hammered).
But as before, the real learning happens when you build. Pick something you do by hand, look at the data as it moves between nodes, and the concepts in this post will stop being abstract.