When data flows from one system to another, assume it can arrive malformed: missing fields, wrong types, unexpected nulls, strings where you expected numbers...
When data flows from one system to another, assume it can arrive malformed: missing fields, wrong types, unexpected nulls, strings where you expected numbers, or arrays where you expected objects. Adding a validation layer between the source and the destination catches these issues before they cause cryptic errors in the destination system or corrupt its data.
Real-world example: Your workflow receives customer data from a webhook and writes it to a CRM. The validation layer ensures every record has valid email, name, and phone before the CRM write.
const items = $input.all();
const valid = [];
const invalid = [];
for (const item of items) {
const data = item.json;
const errors = [];
// Required field checks
if (!data.email || typeof data.email !== 'string') {
errors.push('Missing or invalid email');
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email)) {
errors.push(`Invalid email format: ${data.email}`);
}
if (!data.first_name || typeof data.first_name !== 'string') {
errors.push('Missing first_name');
}
if (!data.last_name || typeof data.last_name !== 'string') {
errors.push('Missing last_name');
}
// Type coercion and normalization
if (data.phone) {
// Strip non-numeric characters
data.phone = String(data.phone).replace(/[^0-9+]/g, '');
if (data.phone.length < 10) {
errors.push(`Phone number too short: ${data.phone}`);
}
}
// Numeric field validation
if (data.revenue !== undefined && data.revenue !== null) {
const revenue = Number(data.revenue);
if (isNaN(revenue) || revenue < 0) {
errors.push(`Invalid revenue value: ${data.revenue}`);
} else {
data.revenue = revenue; // Ensure it is a number, not string
}
}
// Enum validation
const validSources = [
'website', 'referral', 'advertising', 'organic', 'partner'
];
if (data.source && !validSources.includes(data.source)) {
errors.push(
`Invalid source "${data.source}". Must be one of: ${validSources.join(', ')}`
);
}
if (errors.length > 0) {
invalid.push({
json: {
original_data: data,
validation_errors: errors,
rejected_at: new Date().toISOString()
}
});
} else {
valid.push({ json: data });
}
}
// Output 1: valid records -> CRM write
// Output 2: invalid records -> error handling
return [valid, invalid];
```text
```text title="Workflow Structure"
Webhook Trigger
|
Code Node: Validate Data
|
+-> Output 1 (Valid): CRM - Create/Update Contact
|
+-> Output 2 (Invalid): Google Sheets - Log Rejections
+ Slack Alert (if > 10% rejection rate)
```text
```javascript title="Code Node: Rejection Rate Alert"
const validCount = $('Validate Data').first().json.validCount;
const invalidCount = $input.all().length;
const total = validCount + invalidCount;
const rejectionRate = ((invalidCount / total) * 100).toFixed(1);
if (parseFloat(rejectionRate) > 10) {
return [{
json: {
alert: `High rejection rate: ${rejectionRate}% (${invalidCount}/${total} records rejected)`,
sample_errors: $input.all().slice(0, 3).map(
i => i.json.validation_errors
)
}
}];
}
return []; // No alert needed
```text
> **Warning: Validate in Both Directions**
>
> If you run a two-way sync (Tip 3), validate data in both directions. System A's data format assumptions may differ from System B's. Each direction needs its own validation layer tailored to the destination system's requirements.
Validation layers prevent garbage-in-garbage-out scenarios, provide clear error messages for debugging, and create an audit trail of rejected data that you can review and re-process.
**Related:** [Use Path Parameters in Webhook URLs for Dynamic Routing](../webhook-mastery/01-use-path-parameters-in-webhook-urls-for-dynamic-routing.md) | [Use Edit Fields in "Map Each" Mode for Simple Renames](../data-transformation/01-use-edit-fields-in-map-each-mode-for-simple-renames.md)
I build production n8n and Cloudflare automation for teams — the same engineering behind HarperFlow. Fixed-price, escrow-protected, US-based.