2.2 — Data Mapping and JS Expressions
Data Mapping and JS Expressions: The Logic Layer
The difference between a beginner n8n user and a production-grade automation engineer is one thing: JavaScript expressions. Every webhook delivers messy JSON. Every API returns data in its own format. Every client's Google Sheet has different column names. Your job is to transform chaos into clean, standardized data that flows perfectly between nodes. This lesson teaches you the expression language that makes n8n workflows intelligent — the same expressions that a Lahore freelancer uses to process 2,000 leads per month without a single manual data cleanup.
Section 1: The Expression Syntax
n8n uses a template-string-like syntax to access data from previous nodes. Understanding this is non-negotiable — every workflow you build will use expressions.
n8n EXPRESSION SYNTAX — THE COMPLETE REFERENCE
===============================================================
BASIC ACCESS:
├── Current node data: {{ $json.field_name }}
├── Specific node: {{ $node["NodeName"].json.field_name }}
├── Parameters: {{ $node["NodeName"].parameter.param_name }}
└── Environment: {{ $env.MY_VARIABLE }}
DATA NAVIGATION:
├── Nested object: {{ $json.address.city }}
├── Array element: {{ $json.items[0].name }}
├── Array length: {{ $json.items.length }}
└── Optional chain: {{ $json.address?.city ?? "Unknown" }}
WORKFLOW CONTEXT:
├── Execution ID: {{ $execution.id }}
├── Workflow name: {{ $workflow.name }}
├── Current date: {{ new Date().toISOString() }}
├── PKT timestamp: {{ new Date().toLocaleString('en-PK',
│ {timeZone: 'Asia/Karachi'}) }}
└── Item index: {{ $itemIndex }}
WHERE EXPRESSIONS WORK:
├── Set node → field values
├── IF node → conditions
├── HTTP Request → URL, headers, body
├── Gmail → subject, body, to address
├── Google Sheets → column values
└── Basically everywhere you see the {{ }} icon
===============================================================
The Set Node vs. Code Node Decision
| Task | Use Set Node | Use Code Node |
|---|---|---|
| Rename a field | {{ $json.old_name }} | Overkill |
| Simple math | {{ $json.price * 1.17 }} | Overkill |
| String concatenation | {{ $json.first + ' ' + $json.last }} | Overkill |
| Loop through array | Cannot | for (const item of $input.all()) {...} |
| Complex conditional logic | Limited | Full JS if/else/switch |
| Call external function | Cannot | const result = someFunction() |
| Process 100+ items differently | Awkward | $input.all().map(item => ...) |
Rule of thumb: If it's a one-liner, use the Set node. If it needs a loop, conditional branches, or multi-step logic, use the Code node.
Section 2: Essential Data Transformations
These are the transformations you'll use in 80% of Pakistani automation workflows:
Phone Number Normalization (Critical for WATI/WhatsApp)
Pakistani phone numbers arrive in chaos: 03001234567, +923001234567, 0300-123-4567, 92300 1234567, 300 1234567. Your WhatsApp API needs exactly one format: +923001234567.
// UNIVERSAL PAKISTANI PHONE NORMALIZER
// Use in a Code node for reliability
for (const item of $input.all()) {
let phone = String(item.json.phone || '');
// Step 1: Remove all non-digit characters
phone = phone.replace(/[^0-9]/g, '');
// Step 2: Handle different starting patterns
if (phone.startsWith('92') && phone.length === 12) {
phone = '+' + phone; // 923001234567 -> +923001234567
} else if (phone.startsWith('0') && phone.length === 11) {
phone = '+92' + phone.substring(1); // 03001234567 -> +923001234567
} else if (phone.length === 10 && phone.startsWith('3')) {
phone = '+92' + phone; // 3001234567 -> +923001234567
} else {
phone = 'INVALID: ' + item.json.phone; // Flag for manual review
}
item.json.phone_normalized = phone;
item.json.phone_valid = !phone.startsWith('INVALID');
}
return $input.all();
PHONE FORMAT CONVERSION TABLE
===============================================================
INPUT FORMAT -> OUTPUT (E.164) STATUS
────────────────────────────────────────────────────
03001234567 -> +923001234567 Valid
+923001234567 -> +923001234567 Valid
0300-123-4567 -> +923001234567 Valid
92300 1234567 -> +923001234567 Valid
300 1234567 -> +923001234567 Valid
923001234567 -> +923001234567 Valid
1234567 -> INVALID Flagged
+1-555-123-4567 -> INVALID Flagged
WHY THIS MATTERS:
├── WATI API requires +92XXXXXXXXXX format exactly
├── One wrong digit = failed WhatsApp message
├── Failed message = missed customer notification
├── Missed notification = unhappy client = lost retainer
└── This one Code node prevents all of that
===============================================================
Name Cleaning and Title Case
// Clean and format names from messy form submissions
for (const item of $input.all()) {
let name = String(item.json.name || '');
// Trim whitespace, collapse multiple spaces
name = name.trim().replace(/\s+/g, ' ');
// Convert to Title Case
name = name.toLowerCase().replace(/\b\w/g, c => c.toUpperCase());
item.json.name_clean = name;
// Split into first/last for email personalization
const parts = name.split(' ');
item.json.first_name = parts[0] || '';
item.json.last_name = parts.slice(1).join(' ') || '';
}
return $input.all();
Lead Score Conditional Routing
// Expression in Set node (no Code node needed):
// Priority label based on score
{{ $json.score >= 80 ? "HOT"
: $json.score >= 60 ? "WARM"
: "COLD" }}
// Urgency flag for WhatsApp alert
{{ $json.score >= 80 ? "CALL IMMEDIATELY"
: $json.score >= 60 ? "Follow up within 24 hours"
: "Add to nurture list" }}
// PKR formatting
{{ "PKR " + Number($json.amount).toLocaleString('en-PK') }}
// Input: 45000 -> Output: "PKR 45,000"
Domain Extraction from Email
// Extract company domain for enrichment
{{ $json.email.split('@')[1] }}
// ahmed@techstartup.pk -> techstartup.pk
// Extract domain from URL (Code node for reliability)
for (const item of $input.all()) {
let url = String(item.json.website || '');
url = url.replace(/^https?:\/\//, '') // Remove protocol
.replace(/^www\./, '') // Remove www
.split('/')[0] // Remove path
.split(':')[0] // Remove port
.toLowerCase(); // Normalize case
item.json.domain = url;
}
return $input.all();
Section 3: Binary vs. JSON Data
n8n DATA TYPES — WHEN EACH APPLIES
===============================================================
JSON DATA (Text, Numbers, Objects):
├── Webhook payloads
├── API responses
├── Google Sheets data
├── AI-generated text
├── Form submissions
└── Access via: {{ $json.field_name }}
BINARY DATA (Files, Images, PDFs):
├── Email attachments
├── Downloaded files (HTTP Request)
├── Generated PDFs/images
├── Uploaded files
└── Access via: {{ $binary.data }} or {{ $binary.attachment }}
COMMON MISTAKE:
├── You generate a PDF report via an AI/Code node
├── You try to attach it to Gmail using {{ $json.report }}
├── Gmail sends an empty email — no attachment
├── WHY: The PDF is binary data, not JSON
├── FIX: Use {{ $binary.data }} in the Gmail attachment field
└── Or use the "Move Binary Data" node to convert
CONVERSION NODES:
├── "Move Binary Data" — converts between JSON and binary
├── "Read Binary File" — reads file from disk as binary
├── "Write Binary File" — saves binary to disk
└── "Spreadsheet File" — converts CSV/Excel to JSON rows
===============================================================
Section 4: Advanced Expression Patterns
Date and Time Handling for Pakistan (PKT = UTC+5)
// Current PKT timestamp (expression)
{{ new Date().toLocaleString('en-PK', {timeZone: 'Asia/Karachi'}) }}
// Is it business hours in Pakistan? (Code node)
for (const item of $input.all()) {
const pktHour = new Date().toLocaleString('en-US', {
timeZone: 'Asia/Karachi',
hour: 'numeric',
hour12: false
});
const hour = parseInt(pktHour);
item.json.is_business_hours = (hour >= 9 && hour < 18);
item.json.pkt_hour = hour;
// Route: business hours -> immediate action, after hours -> queue
item.json.action = item.json.is_business_hours
? "Send WhatsApp now"
: "Queue for 9 AM PKT tomorrow";
}
return $input.all();
PKR Currency Formatting
// Simple expression for amounts
{{ "PKR " + Number($json.amount).toLocaleString('en-PK') }}
// 1500000 -> "PKR 1,500,000"
// Code node with GST calculation
for (const item of $input.all()) {
const amount = Number(item.json.amount);
const gst = amount * 0.17; // Pakistan GST rate: 17%
item.json.subtotal = `PKR ${amount.toLocaleString('en-PK')}`;
item.json.gst = `PKR ${gst.toLocaleString('en-PK')}`;
item.json.total = `PKR ${(amount + gst).toLocaleString('en-PK')}`;
item.json.total_raw = amount + gst;
}
return $input.all();
Array Processing (Multiple Items)
// Process all line items from a Shopify order
for (const item of $input.all()) {
const lineItems = item.json.line_items || [];
item.json.product_summary = lineItems
.map(li => `${li.name} x${li.quantity}`)
.join(', ');
item.json.total_quantity = lineItems
.reduce((sum, li) => sum + li.quantity, 0);
item.json.item_count = lineItems.length;
}
return $input.all();
// Input: [{name:"Blue Kurta", quantity:2}, {name:"White Shalwar", quantity:1}]
// Output: product_summary = "Blue Kurta x2, White Shalwar x1"
// total_quantity = 3
// item_count = 2
Practice Lab
Exercise 1: Phone Number Normalizer — Create a Manual Trigger with 10 Pakistani phone numbers in different formats (03001234567, +92-321-456-7890, 0300 123 4567, 3451234567, 92-300-123-4567, and 5 more variations). Add a Code node with the Universal Pakistani Phone Normalizer from Section 2. Run the workflow. Verify all valid numbers output as +92XXXXXXXXXX and invalid numbers are flagged. Save this Code node as a reusable snippet — you'll use it in every Pakistani automation.
Exercise 2: Name Cleaner + Title Case — Create a Manual Trigger with messy names: " taqi NAQVI ", "AHMED khan", " maria ALI butt ", "zara". Add a Code node that trims, normalizes to Title Case, and splits into first_name/last_name. Verify outputs: "Taqi Naqvi" (first: "Taqi", last: "Naqvi"), "Ahmed Khan", "Maria Ali Butt" (first: "Maria", last: "Ali Butt"), "Zara" (first: "Zara", last: "").
Exercise 3: Revenue Calculator — Create a workflow that takes a deal size in PKR and a closing probability (0-1). Use expressions to calculate: Expected Value = amount * probability, formatted as "PKR 500,000". Add GST calculation (17%). Add USD conversion at a fixed rate (1 USD = PKR 280). Output should show: subtotal, GST, total PKR, and total USD. Test with PKR 100,000 at 0.7 probability.
Exercise 4: Full Data Pipeline — Build an end-to-end transformation: Webhook receives raw lead data (messy name, unformatted phone, email, deal amount as string). Set node / Code node cleans everything: Title Case name, E.164 phone, domain extracted from email, PKR formatted amount, lead score conditional label (HOT/WARM/COLD), PKT timestamp. Output the clean JSON and append to a Google Sheet. This is the "data cleaning layer" that sits between every trigger and every action node in production workflows.
Pakistan Case Study
Lahore Lead Processing Agency — 2,000 Leads/Month on Autopilot
Usman ran a lead generation service from Johar Town, Lahore. His clients (3 real estate agencies) would upload CSV files of property inquiries to a shared Google Sheet. Usman's job: clean the data, normalize phone numbers, score the leads, and distribute to the right sales agent. He was doing this manually — 2 hours per day, 5 days a week.
Before n8n:
| Task | Time/Day | Monthly Cost (at PKR 400/hour) |
|---|---|---|
| Phone number cleanup | 30 min | PKR 6,000 |
| Name formatting | 15 min | PKR 3,000 |
| Lead scoring (manual review) | 45 min | PKR 9,000 |
| Agent assignment | 15 min | PKR 3,000 |
| WhatsApp notifications | 15 min | PKR 3,000 |
| Total | 2 hours/day | PKR 24,000/month |
His n8n Pipeline (3 hours to build):
| Node | Purpose | Expression Used |
|---|---|---|
| Google Sheets Trigger | Watch for new rows | Poll every 2 min |
| Code Node 1 | Phone normalizer | Universal PK normalizer |
| Code Node 2 | Name cleaner + Title Case | trim + replace + Title Case |
| Set Node | Domain extraction | {{ $json.email.split('@')[1] }} |
| Set Node | PKR formatting | {{ "PKR " + Number($json.amount).toLocaleString() }} |
| IF Node | Lead scoring threshold | {{ $json.score >= 70 }} |
| Code Node 3 | Agent assignment logic | Score + city routing |
| WATI HTTP | WhatsApp to assigned agent | Personalized alert |
| Google Sheets | Write clean data to CRM tab | All transformed fields |
Results After 30 Days:
| Metric | Before (Manual) | After (n8n Expressions) | Change |
|---|---|---|---|
| Leads processed/month | 2,000 (barely) | 2,000 (effortless) | Same volume, zero effort |
| Processing time | 2 hours/day | 0 (fully automated) | -100% |
| Phone format errors | 50-80/month | 0 | -100% |
| Name formatting errors | 30-40/month | 0 | -100% |
| Monthly labor cost | PKR 24,000 | PKR 0 | -100% |
| Capacity for new clients | 0 (maxed out) | 3-4 more at same volume | Unlimited |
Usman's Insight: "Pehle main 2 ghante roz phone numbers theek karta tha — har format alag, WATI reject karta tha. Ab ek Code node hai jo har format ko +92 mein convert kar deta hai. 30 din mein ek bhi failed WhatsApp message nahi aayi. Ab 3 clients ki jagah 6 clients handle kar sakta hun — same effort, double revenue."
Key Takeaways
- JavaScript expressions are the logic layer that makes n8n workflows intelligent — every Set node, IF node, and HTTP Request uses them
- Use the Set node for simple one-liners (field rename, basic math, string concatenation) and the Code node for loops, complex conditionals, and multi-step transformations
- The Universal Pakistani Phone Normalizer (Section 2) is the single most reusable Code node for any Pakistani automation — save it and deploy in every project
- Always normalize data BEFORE it reaches your CRM, WhatsApp API, or email node — one malformatted phone number means one failed customer notification
- Binary data (files, images, PDFs) and JSON data (text, numbers) are different types in n8n — use the "Move Binary Data" node to convert between them
- PKT timezone handling (
{timeZone: 'Asia/Karachi'}) is essential for scheduling and timestamp accuracy in Pakistani workflows - The
toLocaleString('en-PK')method formats PKR amounts correctly with commas — use it everywhere you display money to clients - Array processing in Code nodes (
$input.all().map(),.reduce(),.filter()) handles multi-item data like Shopify line items and bulk CSV imports - A data cleaning layer (phone + name + domain + amount formatting) should sit between every trigger and every action node — it takes 30 minutes to build and prevents 100% of data format errors
Lesson Summary
Quiz: Data Mapping and JS Expressions
5 questions to test your understanding. Score 60% or higher to pass.