This workflow corresponds to n8n.io template #14034 — we link there as the canonical source.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "ECpjYNcNN3deie7g",
"name": "Transform and map data fields standalone via webhook with configurable type conversion",
"tags": [],
"nodes": [
{
"id": "6d91c4c4-ce5c-4002-ba1c-4d6e7719db0a",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-640,
1424
],
"parameters": {
"color": 4,
"width": 820,
"height": 952,
"content": "## Transform and Map Data Fields, Standalone, No External Services\n\nA **self-contained data transformation workflow** that runs entirely inside n8n. No external APIs, no credentials, no third-party services required. Send records via webhook, get transformed and validated data back.\n\nUnlike CRM-specific import workflows, this template is a **standalone data transformer** you can place in front of any system: ERP, CRM, database, or API.\n\n### Who is this for?\nTeams that regularly import data from external sources (suppliers, partners, legacy systems) and need to normalize field names, convert types, and validate records before loading into any target system.\n\n### How it works\n1. **Receive Data**: A webhook accepts a POST request with an array of records (JSON) or a single record\n2. **Configure Mapping**: A Set node defines the field mapping rules, source field, target field, data type, default value, and whether the field is required\n3. **Transform Records**: A Code node applies all mappings: renames fields, converts types (string, number, boolean, date with locale support), fills in defaults, and validates required fields\n4. **Return Result**: The webhook responds with transformed records and a validation report listing any errors per row\n\n### Key features\n- **Zero dependencies**: No credentials, no external services needed\n- **Locale-aware type conversion**: Handles comma decimals (`12,50`), German dates (`DD.MM.YYYY`), and `ja/nein` booleans\n- **Validation report**: Returns per-row errors for required fields and failed type conversions\n- **Flexible input**: Accepts `{ records: [...] }` arrays or single objects\n\n### Setup\n1. Activate the workflow\n2. Edit the **Configure Field Mapping** node to define your mappings\n3. POST your data to the webhook URL\n\n### How to customize\n- Add or remove field mappings in the Set node\n- Set `removeUnmappedFields` to true to strip fields not in the mapping\n- Set `trimStrings` to true to auto-trim whitespace\n- Set `emptyStringToNull` to convert empty strings to null values\n- Adjust `dateInputFormat`, `dateOutputFormat`, and `decimalSeparator` for your locale\n- Connect downstream nodes (database, API, email) after the response to process the output\n\n**Author:** Florian Eiche, [eiche-digital.de](https://eiche-digital.de)"
},
"typeVersion": 1
},
{
"id": "82078d1b-d018-44a8-8bc1-e949bd5c3adb",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
208,
1424
],
"parameters": {
"width": 372,
"height": 548,
"content": "### Step 1: Receive Data\nPOST an array of records or a single object:\n```json\n{\n \"records\": [\n {\n \"Artikelnr\": \"A-1001\",\n \"Bezeichnung\": \"Schraube M8\",\n \"Preis\": \"12,50\",\n \"Gewicht_kg\": \"\",\n \"Aktiv\": \"ja\",\n \"Erstellt_am\": \"15.03.2026\"\n }\n ]\n}\n```\n\nThe webhook accepts both `{ records: [...] }` and a single object `{ field: value }`. Single objects are automatically wrapped in an array."
},
"typeVersion": 1
},
{
"id": "b79d26e2-6033-41f4-b0d0-0b9500ccceee",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
608,
1424
],
"parameters": {
"width": 340,
"height": 548,
"content": "### Step 2: Configure Field Mapping\nEdit the **Configure Field Mapping** node:\n\n**fieldMappings** \u2014 array of rules:\n- `sourceField`: field name in the input data\n- `targetField`: desired field name in the output\n- `dataType`: `string`, `number`, `boolean`, `date`, or `auto`\n- `defaultValue`: value to use when the field is empty or missing\n- `required`: if true, missing values generate an error\n\n**globalSettings**:\n- `removeUnmappedFields`: drop fields not in the mapping\n- `trimStrings`: auto-trim whitespace\n- `emptyStringToNull`: convert `\"\"` to null\n- `dateInputFormat`: expected date format in source data (e.g. `DD.MM.YYYY`)\n- `dateOutputFormat`: output date format (default: `YYYY-MM-DD`)\n- `decimalSeparator`: `.` or `,` for number parsing"
},
"typeVersion": 1
},
{
"id": "976508ce-9c2f-4e3a-830d-e6998b11e019",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
976,
1424
],
"parameters": {
"width": 300,
"height": 540,
"content": "### Step 3: Transform Records\nThe Code node processes each record:\n1. Iterates over all field mappings\n2. Reads the source field value\n3. Applies type conversion (number, boolean, date)\n4. Falls back to defaultValue if empty\n5. Flags required fields that are still missing\n6. Optionally removes unmapped fields\n\nOutput per record:\n```json\n{\n \"data\": { /* transformed fields */ },\n \"errors\": [\"Row 1: 'weight_kg' is required but empty\"]\n}\n```"
},
"typeVersion": 1
},
{
"id": "115b1a34-7165-42b0-bfb9-b4bf7c3836f8",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1312,
1424
],
"parameters": {
"width": 308,
"height": 540,
"content": "### Step 4: Return Result\nThe webhook responds with:\n```json\n{\n \"success\": true,\n \"totalRecords\": 47,\n \"validRecords\": 45,\n \"errorCount\": 2,\n \"records\": [ /* transformed data */ ],\n \"errors\": [\n \"Row 3: 'weight_kg' is required but empty\",\n \"Row 12: 'price' could not be converted to number\"\n ]\n}\n```\n\nConnect downstream nodes after the **Return Transformed Data** node to save or forward the result."
},
"typeVersion": 1
},
{
"id": "eb8d97d1-2c25-4ad5-91bf-0466061a53a9",
"name": "Receive Data",
"type": "n8n-nodes-base.webhook",
"position": [
336,
1984
],
"parameters": {
"path": "transform-data",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2.1
},
{
"id": "76c80090-9671-4b41-8def-3d186a34bf98",
"name": "Configure Field Mapping",
"type": "n8n-nodes-base.set",
"position": [
672,
1984
],
"parameters": {
"mode": "raw",
"options": {},
"jsonOutput": "{\n \"fieldMappings\": [\n {\n \"sourceField\": \"Artikelnr\",\n \"targetField\": \"article_number\",\n \"dataType\": \"string\",\n \"defaultValue\": \"\",\n \"required\": true\n },\n {\n \"sourceField\": \"Bezeichnung\",\n \"targetField\": \"description\",\n \"dataType\": \"string\",\n \"defaultValue\": \"\",\n \"required\": true\n },\n {\n \"sourceField\": \"Preis\",\n \"targetField\": \"price\",\n \"dataType\": \"number\",\n \"defaultValue\": \"0\",\n \"required\": true\n },\n {\n \"sourceField\": \"Gewicht_kg\",\n \"targetField\": \"weight_kg\",\n \"dataType\": \"number\",\n \"defaultValue\": \"0\",\n \"required\": false\n },\n {\n \"sourceField\": \"Aktiv\",\n \"targetField\": \"is_active\",\n \"dataType\": \"boolean\",\n \"defaultValue\": \"true\",\n \"required\": false\n },\n {\n \"sourceField\": \"Erstellt_am\",\n \"targetField\": \"created_at\",\n \"dataType\": \"date\",\n \"defaultValue\": \"\",\n \"required\": false\n }\n ],\n \"globalSettings\": {\n \"removeUnmappedFields\": true,\n \"trimStrings\": true,\n \"emptyStringToNull\": true,\n \"dateInputFormat\": \"DD.MM.YYYY\",\n \"dateOutputFormat\": \"YYYY-MM-DD\",\n \"decimalSeparator\": \",\"\n }\n}"
},
"typeVersion": 3.4
},
{
"id": "313f4228-2e63-45a7-895c-9bebc3a1be93",
"name": "Transform Records",
"type": "n8n-nodes-base.code",
"position": [
1008,
1984
],
"parameters": {
"jsCode": "// Get input data and configuration\nconst raw = $('Receive Data').first().json.body || $('Receive Data').first().json;\nconst config = $('Configure Field Mapping').first().json;\n\nconst mappings = config.fieldMappings || [];\nconst settings = config.globalSettings || {};\n\n// Extract records \u2014 support both { records: [...] } and single object\nlet records = [];\nif (raw.records && Array.isArray(raw.records)) {\n records = raw.records;\n} else if (Array.isArray(raw)) {\n records = raw;\n} else {\n // Single object: wrap in array, exclude meta fields\n const single = {};\n for (const [k, v] of Object.entries(raw)) {\n if (k !== 'records') single[k] = v;\n }\n records = [single];\n}\n\nif (records.length === 0) {\n return [{\n json: {\n success: false,\n totalRecords: 0,\n validRecords: 0,\n errorCount: 1,\n records: [],\n errors: ['No records found in input data']\n }\n }];\n}\n\nconst allErrors = [];\nconst transformedRecords = [];\n\n// --- Helper: parse date from input format to output format ---\nfunction parseDate(value, inputFormat, outputFormat) {\n if (!value || String(value).trim() === '') return null;\n const v = String(value).trim();\n\n // Build regex from input format\n const parts = inputFormat.split(/[.\\-\\/]/);\n const sep = inputFormat.match(/[.\\-\\/]/)?.[0] || '.';\n const valueParts = v.split(sep);\n\n if (valueParts.length !== parts.length) return null;\n\n let day, month, year;\n parts.forEach((p, i) => {\n const upper = p.toUpperCase();\n if (upper.startsWith('D')) day = parseInt(valueParts[i], 10);\n else if (upper.startsWith('M')) month = parseInt(valueParts[i], 10);\n else if (upper.startsWith('Y')) year = parseInt(valueParts[i], 10);\n });\n\n if (!day || !month || !year) return null;\n if (month < 1 || month > 12 || day < 1 || day > 31) return null;\n\n // Build output\n const pad = (n) => String(n).padStart(2, '0');\n return outputFormat\n .replace('YYYY', String(year))\n .replace('MM', pad(month))\n .replace('DD', pad(day));\n}\n\n// --- Helper: parse number with configurable decimal separator ---\nfunction parseNumber(value, decSep) {\n if (value === null || value === undefined) return null;\n let v = String(value).trim();\n if (v === '') return null;\n\n // Remove thousands separators\n if (decSep === ',') {\n v = v.replace(/\\./g, ''); // remove dots (thousands)\n v = v.replace(',', '.'); // comma to dot\n } else {\n v = v.replace(/,/g, ''); // remove commas (thousands)\n }\n\n const num = Number(v);\n return isNaN(num) ? null : num;\n}\n\n// --- Helper: parse boolean ---\nfunction parseBoolean(value) {\n if (value === null || value === undefined) return null;\n const v = String(value).trim().toLowerCase();\n if (['true', '1', 'yes', 'ja', 'y', 'j', 'aktiv', 'active', 'on'].includes(v)) return true;\n if (['false', '0', 'no', 'nein', 'n', 'inaktiv', 'inactive', 'off'].includes(v)) return false;\n return null;\n}\n\n// --- Process each record ---\nrecords.forEach((record, rowIndex) => {\n const rowNum = rowIndex + 1;\n const transformed = {};\n const rowErrors = [];\n\n // Apply each mapping\n mappings.forEach((mapping) => {\n const { sourceField, targetField, dataType, defaultValue, required } = mapping;\n let value = record[sourceField];\n\n // Trim strings\n if (settings.trimStrings && typeof value === 'string') {\n value = value.trim();\n }\n\n // Empty string to null\n if (settings.emptyStringToNull && value === '') {\n value = null;\n }\n\n // Apply default if empty/null/undefined\n if ((value === null || value === undefined || value === '') && defaultValue !== undefined && defaultValue !== '') {\n value = defaultValue;\n }\n\n // Check required\n if (required && (value === null || value === undefined || value === '')) {\n rowErrors.push(`Row ${rowNum}: '${targetField}' is required but empty`);\n transformed[targetField] = null;\n return;\n }\n\n // Type conversion\n if (value !== null && value !== undefined && value !== '') {\n switch (dataType) {\n case 'number': {\n const num = parseNumber(value, settings.decimalSeparator || ',');\n if (num === null) {\n rowErrors.push(`Row ${rowNum}: '${targetField}' could not be converted to number (value: '${value}')`);\n value = null;\n } else {\n value = num;\n }\n break;\n }\n case 'boolean': {\n const bool = parseBoolean(value);\n if (bool === null) {\n rowErrors.push(`Row ${rowNum}: '${targetField}' could not be converted to boolean (value: '${value}')`);\n value = null;\n } else {\n value = bool;\n }\n break;\n }\n case 'date': {\n const inputFmt = settings.dateInputFormat || 'DD.MM.YYYY';\n const outputFmt = settings.dateOutputFormat || 'YYYY-MM-DD';\n const parsed = parseDate(value, inputFmt, outputFmt);\n if (parsed === null) {\n rowErrors.push(`Row ${rowNum}: '${targetField}' could not be parsed as date (value: '${value}', expected: '${inputFmt}')`);\n value = null;\n } else {\n value = parsed;\n }\n break;\n }\n case 'string':\n value = String(value);\n break;\n // 'auto' \u2014 keep as-is\n }\n }\n\n transformed[targetField] = value;\n });\n\n // Optionally keep unmapped fields\n if (!settings.removeUnmappedFields) {\n const mappedSourceFields = mappings.map(m => m.sourceField);\n for (const [key, val] of Object.entries(record)) {\n if (!mappedSourceFields.includes(key)) {\n transformed[key] = val;\n }\n }\n }\n\n transformedRecords.push(transformed);\n allErrors.push(...rowErrors);\n});\n\nconst errorCount = allErrors.length;\nconst validRecords = records.length - new Set(allErrors.map(e => {\n const match = e.match(/Row (\\d+):/);\n return match ? parseInt(match[1]) : null;\n})).size;\n\nreturn [{\n json: {\n success: errorCount === 0,\n totalRecords: records.length,\n validRecords,\n errorCount,\n records: transformedRecords,\n errors: allErrors\n }\n}];"
},
"typeVersion": 2
},
{
"id": "2d4bf337-0bcf-4f0b-afe8-1bf1cd530219",
"name": "Return Transformed Data",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
1344,
1984
],
"parameters": {
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"respondWith": "json",
"responseBody": "={{ JSON.stringify($json) }}"
},
"typeVersion": 1.5
}
],
"active": true,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "12ce1d49-c778-4aff-aa43-84036a90e66e",
"connections": {
"Receive Data": {
"main": [
[
{
"node": "Configure Field Mapping",
"type": "main",
"index": 0
}
]
]
},
"Transform Records": {
"main": [
[
{
"node": "Return Transformed Data",
"type": "main",
"index": 0
}
]
]
},
"Configure Field Mapping": {
"main": [
[
{
"node": "Transform Records",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
A self-contained data transformation workflow that runs entirely inside n8n. No external APIs, no credentials, no third-party services required. Send records via webhook, get transformed and validated data back.
Source: https://n8n.io/workflows/14034/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
A clean, extensible REST-style API routing template for n8n webhooks with up to 3 path levels. Serves API routes via Webhooks with path variables Normalizes incoming requests into "global" REQUEST and
PUQ Docker NextCloud deploy. Uses respondToWebhook, stickyNote, httpRequest, ssh. Webhook trigger; 44 nodes.
puq-docker-immich-deploy. Uses respondToWebhook, ssh, stickyNote. Webhook trigger; 35 nodes.
Analyze_email_headers_for_IPs_and_spoofing__3. Uses stickyNote, respondToWebhook, itemLists, httpRequest. Webhook trigger; 35 nodes.
puq-docker-n8n-deploy. Uses respondToWebhook, ssh, stickyNote. Webhook trigger; 34 nodes.