n8n Workflow Patterns & Best Practices
Comprehensive guide for building reliable, maintainable n8n workflows based on 63+ production workflows.
Core Principles
- Sequential over Parallel - Chain HTTP requests sequentially to avoid race conditions
- Always handle errors - Every workflow needs error handling
- Test thoroughly - Test with real data before activating
- Document credentials - Track which workflows use which credentials
- Version control gotchas - Note n8n-specific issues in workflow notes
Common Patterns
Pattern 1: Sequential HTTP Chain
Use when: Making multiple API calls where later calls depend on earlier responses
Webhook → HTTP 1 → HTTP 2 (refs $('HTTP 1')) → HTTP 3 (refs $('HTTP 2'))Why: Avoids race conditions, ensures data availability
Example:
// HTTP Node 2 - Reference previous node
{
"salesforceId": "{{ $('Get Account').item.json.Id }}",
"cargoWiseData": "{{ $('Get Shipment').item.json }}"
}Pattern 2: Error Handling
Use when: Every workflow (always!)
Main Flow → [On Error] → Error Handler → Notify AdminImplementation:
// Error Handler Node
{
"error": "{{ $json.message }}",
"workflow": "{{ $workflow.name }}",
"timestamp": "{{ $now }}",
"data": "{{ $json }}"
}Pattern 3: Webhook Authentication
Use when: Exposing webhooks to external services
Webhook → IF (check api_key) → [true] → Process → Response
→ [false] → 401 ResponseAPI Key Check:
// IF node condition
{{ $json.body.api_key === 'wsi_848bf8aa2a1b4a4bf45a99a4acc609c4' }}Pattern 4: Data Transformation
Use when: Formatting API responses for downstream use
HTTP Request → Code Node → Next StepCode Node Example:
// Transform CargoWise response
const items = $input.all();
return items.map(item => ({
json: {
blNumber: item.json.BLNumber,
pol: item.json.PortOfLoading,
pod: item.json.PortOfDischarge,
eta: new Date(item.json.ETA).toISOString()
}
}));Pattern 5: Batch Processing
Use when: Processing arrays of items
Trigger → HTTP Get List → Split In Batches → Process Item → LoopSplit In Batches Settings:
- Batch Size: 10 (adjust based on API rate limits)
- Reset: true (for multiple workflow runs)
Pattern 6: Conditional Routing
Use when: Different logic based on data values
HTTP Request → Switch (on status) → [case 1] → Handle Case 1
→ [case 2] → Handle Case 2
→ [default] → Handle DefaultSwitch Node:
// Switch on shipment status
Output: {{ $json.status }}
// Cases:
// - "in_transit"
// - "delivered"
// - "delayed"
// - defaultPattern 7: Scheduled Data Sync
Use when: Periodic data synchronization
Cron Trigger → Get Remote Data → Transform → Update Local DB → NotifyCron Schedule Examples:
0 */4 * * *- Every 4 hours0 2 * * *- Daily at 2 AM0 9 * * 1- Every Monday at 9 AM
Pattern 8: Wait for Webhook Response
Use when: Long-running async operations
Webhook 1 (start) → HTTP Request → Store Execution ID
...
Webhook 2 (callback) → Lookup Execution → Complete FlowGotchas & Solutions
Gotcha 1: Merge Node Branch Limit
Problem: Merge node fails with 4+ input branches
Solution:
DON'T: Merge with 4+ branches
DO: Chain sequentially with Code nodes
✗ HTTP1 → \
HTTP2 → Merge (4 inputs) → FAILS
HTTP3 → /
HTTP4 → /
✓ HTTP1 → Code (merge 1,2) → HTTP3 → Code (merge 1-3) → HTTP4 → Code (final merge)Gotcha 2: $json vs $json.body
Problem: Unclear when to use which
Solution:
// Webhook nodes: data is in body
{{ $json.body.fieldName }}
// HTTP nodes: direct access
{{ $json.fieldName }}
// Manual trigger / Code nodes: depends on previous node
{{ $('NodeName').item.json.fieldName }}Gotcha 3: Expression Syntax
Problem: Wrong quotes or escaping
Solutions:
// ✓ Single quotes in expressions
{{ $json['field-with-dash'] }}
// ✗ Double quotes (won't work)
{{ $json["field-with-dash"] }}
// ✓ Safe access with ?
{{ $json.optional?.field }}
// ✓ Array access
{{ $json.items[0].name }}
// ✓ Filters
{{ $json.items.filter(i => i.status === 'active') }}Gotcha 4: Credential Scope
Problem: Credential works in UI but not in execution
Solution:
- Credentials are user-scoped
- Workflow runs as the user who activated it
- Shared workflows need shared credentials
- Use project credentials for team workflows
Gotcha 5: DateTime Formatting
Problem: Inconsistent date formats between services
Solution:
// ✓ Parse to Date object first
new Date('{{ $json.eta }}').toISOString()
// ✓ Format for SQL
new Date('{{ $json.eta }}').toISOString().split('T')[0]
// ✓ Unix timestamp
Math.floor(new Date('{{ $json.eta }}').getTime() / 1000)
// ✓ Use $now for current time
{{ $now.toISO() }}
{{ $now.toFormat('yyyy-MM-dd') }}Gotcha 6: Empty Arrays
Problem: Workflow fails when API returns empty array
Solution:
// ✓ Check array length
{{ $json.items?.length > 0 ? $json.items[0] : null }}
// ✓ Use default value
{{ $json.items ?? [] }}
// ✓ IF node to skip processing
{{ $json.items.length > 0 }}Gotcha 7: Node References Across Branches
Problem: Can't reference node from different branch
Solution:
// ✗ This won't work if GetAccount is in different branch
{{ $('GetAccount').item.json.id }}
// ✓ Merge branches first, then reference
Merge Node → Then use $('GetAccount')
// ✓ OR: Use Set node to store data
Set → Store Data → Reference later via $jsonGotcha 8: HTTP Request Headers
Problem: API requires specific headers not sent by default
Solution:
// Always set Content-Type for POST/PUT
{
"Content-Type": "application/json"
}
// Authentication headers
{
"Authorization": "Bearer {{ $credentials.token }}"
}
// Custom headers for specific APIs
{
"X-API-Key": "{{ $credentials.apiKey }}",
"User-Agent": "n8n-automation/1.0"
}Performance Optimization
1. Minimize HTTP Requests
Don't:
Loop → HTTP Request (1 per item)Do:
Aggregate → Single HTTP Request (batch API)2. Use Pagination
For large datasets:
// HTTP Request Node
{
"page": "{{ $json.currentPage }}",
"limit": 100
}
// Loop until no more pages
{{ $json.hasMore === true }}3. Cache Frequently Used Data
Pattern:
Cron (hourly) → Fetch Data → Store in DB
...
Trigger → Read from DB (fast) → Process4. Parallel Processing for Independent Tasks
When tasks don't depend on each other:
Trigger → Task 1 → \
→ Task 2 → Merge → Continue
→ Task 3 → /Security Best Practices
1. Never Hardcode Secrets
Don't:
// ✗ Hardcoded API key
const apiKey = "sk_live_abc123";Do:
// ✓ Use credentials
{{ $credentials.apiKey }}2. Validate Webhook Signatures
For critical webhooks:
// Code Node - Verify HMAC signature
const crypto = require('crypto');
const signature = $json.headers['x-webhook-signature'];
const body = JSON.stringify($json.body);
const secret = $credentials.webhookSecret;
const computed = crypto
.createHmac('sha256', secret)
.update(body)
.digest('hex');
if (signature !== computed) {
throw new Error('Invalid signature');
}
return items;3. Rate Limit Webhooks
Prevent abuse:
Webhook → Check Rate Limit (Redis) → [pass] → Process
→ [fail] → 429 Response4. Sanitize User Input
Before using in SQL or HTTP:
// Sanitize input
const sanitized = String($json.input)
.replace(/[^\w\s-]/g, '')
.trim()
.substring(0, 255);Error Handling Patterns
Pattern 1: Retry Logic
HTTP Request → [Error] → Wait (60s) → Retry HTTP → [Error] → Notify AdminPattern 2: Fallback Data Source
HTTP Primary API → [Error] → HTTP Backup API → [Error] → Use CachePattern 3: Partial Failure Handling
// Code Node - Process with error handling
const results = [];
const errors = [];
for (const item of items) {
try {
// Process item
results.push(processItem(item));
} catch (error) {
errors.push({ item, error: error.message });
}
}
return [
{ json: { results, errors } }
];Testing Workflows
1. Test with Manual Trigger
Before activating:
1. Disable webhook/schedule trigger
2. Add Manual Trigger
3. Test with sample data
4. Verify all nodes execute
5. Check error handling
6. Remove manual trigger
7. Activate workflow2. Test Error Paths
Simulate failures:
IF Node → [true] → Normal Flow
→ [false] → Error Path (for testing)3. Use Workflow Notes
Document test cases:
## Test Cases
1. ✓ Valid webhook payload
2. ✓ Invalid API key
3. ✓ Empty result set
4. ✓ Network timeout
5. ✓ Malformed JSON
## Last Tested: 2026-02-28Monitoring & Debugging
1. Add Logging Nodes
Critical Step → Set (log data) → Continue2. Use Webhook Responses for Debugging
// Return debug info in webhook response
{
"success": true,
"processedItems": items.length,
"debug": {
"timestamp": $now.toISO(),
"workflow": $workflow.name,
"execution": $execution.id
}
}3. Track Execution Metrics
Workflow End → HTTP Request → Send Metrics to MonitoringCredential Management
Standard Credentials
| Credential | ID | Used By |
|---|---|---|
| Anthropic API | HaU0Ag4EFvXrn0PB | All AI workflows |
| MS SQL (CargoWise) | Qr4AWlNt0hSngnjs | WSI workflows |
| Gmail SMTP | xxCVAVKBysDgnuXz | Email workflows |
| SSH srv1139900 | ssh-srv1139900-pk | Deployment workflows |
Credential Best Practices
- Use descriptive names - "CargoWise Production" not "SQL1"
- Document scope - Which workflows use which credentials
- Rotate regularly - Update API keys quarterly
- Test after rotation - Verify workflows still work
- Use separate creds for prod/staging - Never share
Common n8n SQL Patterns
Execute Query (CargoWise)
-- Use parameterized queries
SELECT
s.ShipmentNumber,
s.Status,
s.POL,
s.POD
FROM ShipmentHeader s
WHERE s.UpdatedDate >= @since
ORDER BY s.UpdatedDate DESCParameters:
{
"since": "{{ $now.minus({ days: 7 }).toISO() }}"
}Bulk Insert Pattern
// Code Node - Build bulk insert
const values = items.map(item =>
`('${item.json.id}', '${item.json.name}')`
).join(',');
return [{
json: {
query: `INSERT INTO table (id, name) VALUES ${values}`
}
}];Workflow Organization
Naming Convention
[Category] - [Action] - [Target]
Examples:
- Ocean Tracking - Scrape - MSC Container
- WSI - Ingest - CargoWise Updates
- Sales - Sync - Salesforce Accounts
- Notify - Email - Daily ReportWorkflow Notes Template
# [Workflow Name]
## Purpose
Brief description of what this workflow does
## Trigger
- Type: Webhook/Cron/Manual
- Schedule: (if cron)
- Endpoint: (if webhook)
## Dependencies
- Credentials: [list]
- External APIs: [list]
- Database tables: [list]
## Error Handling
- Errors sent to: [email/Telegram/log]
- Retry logic: [yes/no]
## Last Updated
2026-02-28Last Updated: 2026-02-28 Related Docs: n8n Workflows, Reference: n8n Automation