Advanced n8n Workflow Architecture
Advanced Data Transformation
Beyond Simple Connections
You’ve already mastered connecting nodes and pulling data from the previous step. But real-world APIs don’t always hand you clean, flat data. They send nested objects, inconsistent date formats, and arrays that require complex logic to parse. This is where mastering the Expression Editor transforms your workflows from simple chains into resilient, production-grade automations.
The beauty is that n8n normalizes the data flow between nodes into a JSON structure, making it easier to integrate disparate systems.
We'll move past basic $json references and explore how to pull data from any node in your workflow, handle complex data structures, and use powerful libraries like Luxon to tame unruly date and time information.
Navigating Nested JSON
APIs often return JSON with data buried several layers deep. While simple dot notation like {{$json.customer.address.city}} works for predictable structures, you need more power when dealing with arrays or variable keys.
Consider this JSON data returned from an API:
{
"orderId": "ORD-123",
"customer": {
"id": "CUST-456",
"name": "Jane Doe"
},
"items": [
{
"sku": "A-001",
"name": "Workflow Widget",
"quantity": 2,
"tags": ["automation", "tools"]
},
{
"sku": "B-002",
"name": "API Adapter",
"quantity": 1,
"tags": ["integration"]
}
],
"timestamps": {
"created": "2023-10-27T10:00:00Z",
"shipped": "2023-10-28T15:30:00Z"
}
}
To get the SKU of the first item, you’d use {{$json.items[0].sku}}. But what if you need the name of every item in the order? You can use a -like syntax to project the name field from every object in the items array.
// Returns an array of item names:
// ["Workflow Widget", "API Adapter"]
{{$json.items.map(item => item.name)}}
// Older n8n versions used a syntax like this:
// {{$json.items[*].name}}
Modern n8n expressions heavily favor standard JavaScript array methods like
.map(),.filter(), and.find()for their power and readability. While some JMESPath-like syntax remains, using JS methods is the recommended approach.
Referencing Across Your Workflow
The $json variable is convenient, but it can only see the output of the single node right before it. What if you need data from a node three steps back? This is a common requirement for enriching data. For example, a workflow might fetch a user ID, then use that ID to get user details, and finally create a custom email that needs both the original ID and the user's name.
You can access the data from any node in your workflow using the $node variable. This object holds the output of all previously executed nodes, indexed by their names.
In this visual, the Format Output node can pull the userId directly from the Get User ID node, even though Fetch API Data is between them. This prevents you from having to pass the same data through every intermediate step.
When you're dealing with items processed in a loop (for example, using the Split in Batches node), you'll use $item(0).json. The $item variable refers to the specific data item being processed in that loop iteration.
Taming Time with Luxon
Dates and times are a common source of pain in automation. Different services use different formats, timezones, and standards. n8n includes the powerful library directly in the Expression Editor to solve this.
Let's say an API gives you a timestamp like 2023-10-27T10:00:00Z, but you need to display it in a human-readable format for a Slack message.
First, you create a Luxon DateTime object from the input string. Then, you can format it however you need.
// The expression to format the date
{{DateTime.fromISO($json.timestamps.created).toFormat('DDDD, t')}}
// Result:
// "October 27, 2023, 10:00 AM"
You can also perform calculations. Need to find out the date 30 days from now?
// Get today and add 30 days
{{DateTime.now().plus({days: 30}).toISODate()}}
// Result (example):
// "2024-08-17"
Dynamic and Conditional Logic
Your workflows need to be resilient. What happens if an API sometimes returns a field and sometimes doesn't? Or if you need to map data differently based on some condition? You can handle this with conditional expressions.
JavaScript's ternary operator is perfect for this. It follows the structure: condition ? value_if_true : value_if_false.
Imagine an API returns a user object. If the company field exists, you want to use it. Otherwise, you want to default to "N/A".
// Safely access the company name
{{$json.company ? $json.company : "N/A"}}
// A more modern and concise way using the nullish coalescing operator:
{{$json.company ?? "N/A"}}
This technique, known as , ensures that the data you pass to the next node has a predictable structure. By cleaning and standardizing data as it flows through your workflow, you prevent errors in later steps and make your automations much more reliable. You're not just moving data; you're refining it.
By combining deep referencing with $node, powerful transformations with Luxon and JavaScript methods, and resilient logic with conditional expressions, you can build workflows that handle the complexity and unpredictability of real-world data.
In a workflow, you need to use data from a node named "Fetch User List" which ran three steps before the current node. How would you access its output?
An API returns a user object. If the email field exists, you want to use it. Otherwise, you want the expression to return the string 'No Email Provided'. Which expression correctly implements this logic?