A lot of the automations or workflow experiments on digitising events have been orchestrated using make.com. And it works well; however, one of the things that goes against it, especially when you are building with AI (where you will more often than not have to run through many loops and iterations), is its pricing model. Make.com charges one credit for each module that's executed, and we were burning through our credits at an alarming rate, so we wanted to look for a different alternative.
We'd always been aware of n8n and resisted using that in our work because it is much more developer-centric, not just in how it works but in how it's presented. You hit code pretty quickly, or what looks like code.
However the credit munching behaviour of Make.com tipped us towards N8n, or at the very least using a hybrid model where we use both.
Applying it in practice, we quickly ran into some specific nuances on n8n, which is true of any platform. We felt it would be useful to share some of the best practices that we found through our own research, AI research, and building on n8n to replicate / transfer relatively complex Make.com workflows into n8n.
Why? Because following these little bits of advice will save you a tonne of time down the road.
Our biggest advice: Create a "Config" or "Set Variables" Node At the beginning of your workflow.
Place a Set (Edit Fields) node at the very start of every workflow (that goes beyond 5 steps) and use it to define all configuration values. This is something I do on make.com as well, using their Set multiple variables module, and it's exactly the same outcome you're solving for here on n8n. You really don't want to have to remember which node, using the n8n convention, you put a particular value into and then go rummaging around in some complex workflows because you just want to change a setting. By setting variables at the start and referencing them downstream, changing settings becomes trivial.
What was new to us using n8n is the suggested best practice of adding - board IDs, environment paths, API endpoints, thresholds. Then reference them everywhere with.
$('Config').item.json.boardId . This gives you a single place to change values without hunting through every node. Note $('Config') is the name of the node you have set these variables in our case this was { {$('Set Flow Variables').item.json.inputUri } }
// In your opening Set node named "Config":
// boardId: "abc123"
// environment: "production"
// Anywhere downstream:"{ {$('Config').item.json.boardId} }"1. Node References: The Most Critical Gotcha
Always Use Explicit Named References
This is the single most important habit to establish from day one. By default, n8n inserts $json.fieldName when you drag a value into an expression. This is an implicit shorthand that means 'the item coming into this node right now.' The moment you insert a new node upstream, reorder nodes, or branch the workflow, the reference breaks silently; it now pulls from the wrong predecessor. This is very different from make.com where the references are updated dynamically when you reorder or move modules around.
The correct reference pattern:
// ❌ Fragile — breaks if you insert or move a node"outputUri":
"{ {$json.outputUri} }"
// ✅ Stable — always points to the named node regardless of position"outputUri":
"{ {$('node name').item.json.outputUri}}"
The $('Node Name') syntax is the new standard. It uses n8n's item-linking logic (paired items) and is the actively maintained approach. The older $node["Node Name"] javascript style syntax is deprecated It still works, but it matches items by index rather than by item lineage, which causes errors if the number or order of items changes.
The Two Reference Syntaxes Compared
2. Node Naming Conventions
Use Descriptive, Unique, Sentence-Style Names
Node names are not just labels in n8n, they are the reference targets in your expressions. A poorly named node becomes a maintenance nightmare. This is also good practice in general and is becoming increasingly relevant, because if you choose to copy and slap the whole workflow into an LLM to get it to troubleshoot, the more descriptive you are, the better it will perform.
Some Rules to follow:
- Be descriptive and specific. Replace HTTP Request 2 with Run Transcoder Job: 1600 x 900 or Send Slack Alert. Future-you, More importantly, your team, will understand it immediately.
- Write it as a readable story. Reading node names top-to-bottom should describe what the workflow does in plain English. If → bad. Does user have email? → good.
- Names are case-sensitive. $('Set Variables') and $('set variables') reference different nodes, not surprising in a JavaScript-based system. Inconsistent casing is a silent, hard-to-debug gotcha.
- Names must be exact. If a node name has spaces, they must be preserved in the reference string. Renaming a node after referencing it will auto-update most references in the UI, but any hardcoded references in Code nodes will silently break.
- Avoid special characters. Keep names alphanumeric with spaces. Exotic characters in JavaScript systems just avoid, avoid, avoid. Characters like dollar signs, asterisks, even dots, because they have a meaning within Javascript / JSON. If you are labelling your nodes using those characters, you can end up in a world of pain.
- On camelCase vs sentence-case: Sentence-case (e.g., Get Customer Data) is the community convention. Personally I prefer camelCase (getCustomerData) for programmatic consistency. So what I do is use camel case for variables, which just removes the space when referencing them, but I'm going with the community convention of sentence case in naming the nodes.
3. Expression Syntax Gotchas
The {{}} Toggle Trap
n8n fields default to Fixed Value mode in nodes. If you type $json.name into a field that is still in fixed mode, it is treated as a literal string, not evaluated as an expression. Always toggle the field to Expression mode first (the expression toggle icon), or use the / shortcut in the expression editor.
Webhook Data Lives Under .body
The single most common expression error for webhook-triggered workflows: the Webhook node wraps incoming payloads under a .body property to preserve metadata like headers and query parameters. Trying to access $json.email when you want $json.body.email will return undefined.
// ❌ Root access — undefined for webhook payloads
$json.email
// ✅ Correct path for webhook body data
$json.body.email
// Headers
$json.headers['x-api-key']
// Query params
$json.query.pageAccessing Nested Data
$json.company returns the entire company object, not the company name. Always drill all the way down to the exact field using dot notation. Again, this is typical behaviour for a JavaScript-based system:
// ❌ Returns the whole object
$json.company
// ✅ Returns the string value
$json.company.nameFields with Spaces or Special Characters
Use bracket notation, Common with JavaScript-based systems, for field names that contain spaces or special characters:
$json['field name with spaces']
$('My Node').item.json['some-hyphenated-key']4. Code Node Patterns & Gotchas
The Code node, as the name implies, allows you to put a bunch of code into a node and is probably the most powerful feature within n8n. Until recently, make.com didn't have an equivalent, and they have since released one, because with the world moving so fast, there are just times when a little snippet of code solves a whole load of things much faster within the workflow. Happily for us, even if you're a non-coder, you now have an LLM that can write the thing for you, but there are still some nits that you need to know.
Return Format Is Mandatory
The Code node must return an array of objects with a json property. Missing this wrapper is the most common Code node failure. We found this our that hard way.
// ❌ Breaks downstream nodes
return { name: "Adam" };
// ✅ Correct format
return [{ json: { name: "Adam" } }];Never Use {{}} Inside a Code Node
Expression syntax ({{ }}) is for the expression editor in non-code fields. Inside a Code node you write plain JavaScript (or Python if that's your thing). Using {{}} will throw a syntax error.
// ❌ Wrong this is not the expression editor
const name = { { $json.name } };
// ✅ Correct — plain JS access in Code nodes
const items = $input.all();
const name = items.json.name;
const aspectRatios = { landscape: '16:9', square: '1:1', portrait: '9:16' };$input vs Legacy items
The Code node replaced the old Function node. If you're following older tutorials, items are no longer defined directly. Use $input.all() for all items or $input.first() for the first item:
// ❌ Legacy — throws "items is not defined"
items.json.field
// ✅ Modern
const items = $input.all();items.json.field
// Or reference a specific node in Code
context$('Node Name').all().json.fieldAlways Set pairedItem in Code Nodes
When your Code node outputs more items than it received, or restructures items, downstream expressions using $('YourCodeNode').item will break unless you manually set pairedItem. This is the item-linking mechanism that keeps the data lineage intact (Simple, layman's terms: this is like a cross-reference. )
// Links each output back to its source input
return items.map((item, index) => ({ json: { transformed: item.json.value }, pairedItem: index }));5. Error Handling Patterns
Never Deploy Without an Error Workflow
Every production workflow should have an Error Workflow set in Workflow Settings → Error Workflow. This catches any unhandled exception and sends a notification containing the workflow name, the failing node, the error message, and a timestamp. Without this, failures disappear silently. We were caught out by this when we first spun up our n8n, thinking that, as with make.com, we'd get emails when something broke, but that's not the case.
What we did is just create a simple workflow that takes the error and sends us an email. What seems a little bit tedious at first is actually quite nice, because the other thing that you can do is you can have different types of error workflows. For example, in certain workflows you may not want to get an email; you may want to get the error written into and sent to a Slack message or what have you. In this way, you can really define what happens when errors get generated off the back of each workflow, and that can be useful.
The Try/Catch Pattern for HTTP Calls
n8n does not automatically treat a 400 or 500 HTTP response as a workflow error — only thrown exceptions stop the flow. Implement explicit status checking:
- Enable Retry on Fail on HTTP API Requests (settings tab on the node)
- OR Enable Continue On Fail on the HTTP Request node (see below)
- Add an IF node checking $json.error or the status code
- Route the error branch to a notification/logging step
- Route the success branch to continue processing
This is similar to the best practice that we do within our make.com workflows. If you think about it, while we are somewhat spoiled with the robustness of modern APIs, most of the time when you do these kinds of workflows, you are sending over a request to an API endpoint, getting it to do something, and taking it back. There are rare occasions when that API will momentarily fail or not be available, like when you're sending a transcoder job.
Setting the request on fail and timeout, what that basically does is it waits five seconds. That's what I usually recommend: set it to five seconds. It tries the API again, and nine times out of ten it will work and you won't have a failure.
When NOT to Use Continue On Fail
This setting is powerful but dangerous in financial, order, or CRM workflows. Partial writes create reconciliation problems. Use Continue On Fail only when a failed item should be skipped and logged, not retried or reversed.
Advanced: Implement Exponential Backoff for Retries
For transient API failures, use a Wait node with increasing delays rather than immediate retries. Most APIs throttle repeated instant retries more aggressively. Heavily used APIs will often put on rate limits, like, for example, the image generation APIs. This is a useful tactic, but it is not one that I would recommend as default. I would put this in where you know that the APIs are heavy. Again, talking about image generation in our specific use case of video generation, this is a very useful tactic in those situations.
6. Sub-Workflow / Microservices Architecture
Often, people who come to automation platforms like N8n or Make.com are not programmers and are, as one comment on my YouTube channel said, "business owner masquerading as a developer". If this describes you, this is some advice that could save you a lot of time and effort.

As a rule of thumb: Break Large Workflows into Sub-Workflows
Beyond 20-30 nodes, a single workflow becomes difficult to debug and maintain. Extract reusable logic into sub-workflows with strict input/output contracts. Ideal candidates for extraction:
- Logic used in multiple parent workflows (e.g., "resolve user ID to Slack ID", “transcode a video”, “update related content on an article”)
- Heavy processing that benefits from fire-and-forget execution
- Error notification/logging pipelines
Data Passing Gotcha
When using the Execute Sub-workflow node, only the data in the currently active $json is passed to the sub-workflow. If you need data from multiple upstream nodes, use a Merge node first to combine them into a single item, then pass that to the sub-workflow.
Test Sub-Workflows in Isolation
Never test a sub-workflow only by running the parent. Open the sub-workflow directly, simulate the trigger with a Manual Trigger node and pinned test data, and validate it independently before integrating.
7. Variable and Data Management
The "Config" Node is Your Friend
As described above, a Set node named Config at the start of a workflow is the most widely recommended pattern for managing workflow-scoped variables.
- Centralises all configurable values in one place
- Makes swapping staging vs production values trivial
- Ensures downstream references are always explicit named references
Workflow Static Data for Loop State
When you need a variable that persists and updates across loop iterations (not just a config value), use Workflow Static Data inside a Code node. This is the only built-in mechanism for maintaining mutable state within a single execution.
Enterprise vs. Community Variable Scope
Custom Variables (instance-wide constants available in all workflows) are a Pro/Enterprise feature. On the community self-hosted edition, use environment variables ($env.MY_VAR) for instance-level constants, and Set nodes for workflow-level constants.
8. Performance & Scale
Use SplitInBatches for Any Dataset Over ~50 Items
Processing all items in one shot exhausts memory and triggers execution timeouts on large datasets. The SplitInBatches node divides input into chunks and loops:
- General APIs: batch size 50–100
- Database inserts: batch size 100–500
- LLM calls or large payloads: batch size 10–20
- Billing/financial operations: batch size 1 (fail fast, never partially commit)
Add a Wait node after the HTTP Request in your loop to introduce deliberate pauses and avoid rate limit responses.
Large Binary Data (Like AI Image Generation)
For workflows processing files, set N8N_DEFAULT_BINARY_DATA_MODE=filesystem in your environment. This stores binary data on disk rather than in memory and the database, preventing RAM exhaustion. In layman's terms you want the file to be stored on your disk and not in your memory.
Parallel Execution
Independent branches can run simultaneously by connecting a single node to multiple downstream nodes. This is preferable to sequential processing for tasks that have no data dependency between them.
9. Security
Never Hardcode Secrets in Nodes
Typing API keys or passwords directly into a Set node, HTTP Request, or Code node leaks them into workflow JSON exports, screenshots, and version control. The safe pattern for the community edition is.
Use n8n Credentials system, for any external service connection. Credentials are encrypted at rest and rotatable without editing workflow nodes.
Secure Every Webhook
Publicly accessible webhook URLs should always use at minimum one of:
- Header Auth / Basic Auth, built into the Webhook node's Authentication dropdown
- JWT Auth, validate bearer tokens via JWKS
- HMAC signature verification, confirm the request came from the expected sender by verifying a shared-secret signature on the payload
Never rely on a secret URL alone. Treat the webhook URL as potentially leaked and enforce explicit auth.
10. Testing & Debugging
Pin Data After Expensive Operations
Use the Data Pinning feature (the pin icon in the Output panel) to freeze node output during development. This is essential after:
- Webhook triggers (so you don't need to re-fire external events)
- API calls that cost money or have rate limits
- Long-running operations (LLM calls, DB queries)
Pinned data is only used during test executions. Production runs always re-execute normally.
Build Incrementally, Pin as You Go
The recommended development loop:
- Build one node, configure it
- Run and verify its output
- Pin the verified output
- Add the next node, test with Test Step
- Repeat before running the full workflow
Use Smaller Datasets During Development
Reduce batch sizes to 1–5 items and use pinned mini-datasets while building. Scale up to real data only in staging.
Debug Large Workflows
For workflows with performance issues, disable Save Execution Progress in Workflow Settings to reduce database I/O. Use a Code node with console.log(JSON.stringify($input.all())) at the start of sub-workflows to verify exactly what data is being passed from the parent.
11. Versioning & Deployment, for when things get serious.
Treat Workflows as Code
Export workflow JSON to Git for version control. n8n Enterprise includes a built-in Source Control feature (Settings → Source Control) that connects directly to a Git repo and supports branch-based promotion (dev → staging → prod).
For community edition, which we are using, use separate n8n instances per environment and export workflow JSON manually or via the n8n API (POST /source-control/pull).
Credentials Are Not Versioned
When using n8n's Git source control, credentials are intentionally excluded from the repository by design. Manage credentials separately per environment. Never commit credential values to version control.
Tag Everything
Use n8n's workflow tags to encode environment, service, version, and deployment status. Example tag schema: env:production, service:events-pipeline, v1.4, status:deployed. This makes filtering and auditing at scale tractable.
Quick Reference: A n8n Gotchas Summary
