Skip to content

n8n Workflow Patterns & Best Practices ​

Comprehensive guide for building reliable, maintainable n8n workflows based on 63+ production workflows.

Core Principles ​

  1. Sequential over Parallel - Chain HTTP requests sequentially to avoid race conditions
  2. Always handle errors - Every workflow needs error handling
  3. Test thoroughly - Test with real data before activating
  4. Document credentials - Track which workflows use which credentials
  5. 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:

javascript
// 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 Admin

Implementation:

javascript
// 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 Response

API Key Check:

javascript
// 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 Step

Code Node Example:

javascript
// 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 → Loop

Split 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 Default

Switch Node:

javascript
// Switch on shipment status
Output: {{ $json.status }}

// Cases:
// - "in_transit"
// - "delivered"
// - "delayed"
// - default

Pattern 7: Scheduled Data Sync ​

Use when: Periodic data synchronization

Cron Trigger → Get Remote Data → Transform → Update Local DB → Notify

Cron Schedule Examples:

  • 0 */4 * * * - Every 4 hours
  • 0 2 * * * - Daily at 2 AM
  • 0 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 Flow

Gotchas & 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:

javascript
// 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:

javascript
// ✓ 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:

javascript
// ✓ 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:

javascript
// ✓ 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:

javascript
// ✗ 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 $json

Gotcha 8: HTTP Request Headers ​

Problem: API requires specific headers not sent by default

Solution:

json
// 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:

javascript
// 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) → Process

4. 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:

javascript
// ✗ Hardcoded API key
const apiKey = "sk_live_abc123";

Do:

javascript
// ✓ Use credentials
{{ $credentials.apiKey }}

2. Validate Webhook Signatures ​

For critical webhooks:

javascript
// 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 Response

4. Sanitize User Input ​

Before using in SQL or HTTP:

javascript
// 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 Admin

Pattern 2: Fallback Data Source ​

HTTP Primary API → [Error] → HTTP Backup API → [Error] → Use Cache

Pattern 3: Partial Failure Handling ​

javascript
// 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 workflow

2. Test Error Paths ​

Simulate failures:

IF Node → [true] → Normal Flow
       → [false] → Error Path (for testing)

3. Use Workflow Notes ​

Document test cases:

markdown
## Test Cases
1. ✓ Valid webhook payload
2. ✓ Invalid API key
3. ✓ Empty result set
4. ✓ Network timeout
5. ✓ Malformed JSON

## Last Tested: 2026-02-28

Monitoring & Debugging ​

1. Add Logging Nodes ​

Critical Step → Set (log data) → Continue

2. Use Webhook Responses for Debugging ​

javascript
// 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 Monitoring

Credential Management ​

Standard Credentials ​

CredentialIDUsed By
Anthropic APIHaU0Ag4EFvXrn0PBAll AI workflows
MS SQL (CargoWise)Qr4AWlNt0hSngnjsWSI workflows
Gmail SMTPxxCVAVKBysDgnuXzEmail workflows
SSH srv1139900ssh-srv1139900-pkDeployment workflows

Credential Best Practices ​

  1. Use descriptive names - "CargoWise Production" not "SQL1"
  2. Document scope - Which workflows use which credentials
  3. Rotate regularly - Update API keys quarterly
  4. Test after rotation - Verify workflows still work
  5. Use separate creds for prod/staging - Never share

Common n8n SQL Patterns ​

Execute Query (CargoWise) ​

sql
-- Use parameterized queries
SELECT
  s.ShipmentNumber,
  s.Status,
  s.POL,
  s.POD
FROM ShipmentHeader s
WHERE s.UpdatedDate >= @since
ORDER BY s.UpdatedDate DESC

Parameters:

json
{
  "since": "{{ $now.minus({ days: 7 }).toISO() }}"
}

Bulk Insert Pattern ​

javascript
// 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 Report

Workflow Notes Template ​

markdown
# [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-28

Last Updated: 2026-02-28 Related Docs: n8n Workflows, Reference: n8n Automation

HandyManny Documentation System