This workflow corresponds to n8n.io template #5144 — 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 →
{
"nodes": [
{
"id": "61e73730-2e9b-4882-82d0-b218958c0dc1",
"name": "Convert JSON to XML",
"type": "n8n-nodes-base.code",
"position": [
160,
48
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// --- Configuration ---\nconst REMOVE_EMPTY_VALUES = true; // Omit empty tags\n\n// Newline configuration (number of '\\n')\nconst NEWLINES_TOP_LEVEL = 2; // Between top-level elements (e.g., 2 for \\n\\n)\nconst NEWLINES_ARRAY_ITEMS = 2; // Between <0>, <1>... elements of a complex array\nconst NEWLINES_OBJECT_PROPERTIES = 1; // Between <key1>, <key2>... properties of an object\nconst NEWLINES_WITHIN_TAGS = 1; // Between opening/closing tag and its content (0, 1, 2...)\n\n// --- Input Retrieval ---\nconst inputJson = $input.item.json;\n\n// --- Utility Functions ---\n\n/**\n * Generates a string containing the specified number of newlines.\n * @param {number} count The desired number of newlines.\n * @returns {string} The newline string (e.g., \"\\n\", \"\\n\\n\").\n */\nfunction generateNewlines(count) {\n return '\\n'.repeat(Math.max(0, count)); // Ensures a non-negative count\n}\n\n/**\n * Attempts to detect and format a value into a readable date (YYYY-MM-DD HH:mm:ss).\n * @param {any} value The value to check and format.\n * @returns {string|any} The formatted date if detected, otherwise the original value.\n */\nfunction formatIfDate(value) {\n const originalValue = value;\n let date = null;\n try {\n if (value instanceof Date && !isNaN(value.getTime())) {\n date = value;\n }\n /* // Numeric timestamp detection (COMMENTED OUT)\n else if (typeof value === 'number') { ... } */\n else if (typeof value === 'string') {\n // Basic check for string length and presence of digits/date separators\n if (value.length >= 8 && /[0-9]/.test(value) && /[-/T:.\\s]/.test(value)) {\n const parsedDate = new Date(value);\n if (!isNaN(parsedDate.getTime())) {\n date = parsedDate;\n }\n }\n }\n if (date instanceof Date && !isNaN(date.getTime())) {\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, '0');\n const day = String(date.getDate()).padStart(2, '0');\n const hours = String(date.getHours()).padStart(2, '0');\n const minutes = String(date.getMinutes()).padStart(2, '0');\n const seconds = String(date.getSeconds()).padStart(2, '0');\n return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;\n }\n } catch (e) {\n console.warn(`Error formatting date: ${value}`, e);\n return originalValue;\n }\n return originalValue;\n}\n\n\n/**\n * Finds a safe delimiter to join elements of a primitive array.\n * @param {Array<any>} arr The array whose elements will be joined.\n * @returns {string} The found delimiter.\n */\nfunction findArrayDelimiter(arr) {\n const stringElements = arr.map(el => String(el === null || el === undefined ? '' : el));\n let delimiters = [',', '|'];\n // Try common delimiters first\n for (const d of delimiters) {\n if (!stringElements.some(s => s.includes(d))) {\n return d;\n }\n }\n // If common delimiters are found in elements, create a unique one\n let currentDelimiter = '||';\n while (stringElements.some(s => s.includes(currentDelimiter))) {\n currentDelimiter += '|';\n if (currentDelimiter.length > 20) {\n console.warn(\"Long delimiter used:\", arr);\n return currentDelimiter; // Prevent infinite loop for extremely complex cases\n }\n }\n return currentDelimiter;\n}\n\n/**\n * Checks if a value is a primitive (string, number, boolean, null, undefined).\n * @param {any} value The value to check.\n * @returns {boolean} True if it's a primitive.\n */\nfunction isPrimitive(value) {\n const type = typeof value;\n return value === null || type === 'undefined' || type === 'string' || type === 'number' || type === 'boolean';\n}\n\n\n/**\n * Recursive function to convert a JSON value into an XML fragment.\n * Uses configuration parameters for newlines.\n * Does NOT use XML escaping.\n * @param {any} value The JSON value to convert.\n * @param {string} tagName The XML tag name.\n * @returns {string} The generated XML fragment, or an empty string if the value is empty and should be omitted.\n */\nfunction jsonToXmlRecursive(value, tagName) {\n const openTag = `<${tagName}>`;\n const closeTag = `</${tagName}>`;\n\n let processedValue = value;\n\n // 1. Attempt to parse JSON/Array strings\n if (typeof value === 'string') {\n const trimmedValue = value.trim();\n if (trimmedValue.startsWith('{') || trimmedValue.startsWith('[')) {\n try {\n processedValue = JSON.parse(trimmedValue);\n console.log(`String parsed for tag: '${tagName}'.`);\n } catch (e) {\n processedValue = value; // Keep the original string if parsing fails\n console.warn(`Parsing failed for tag: '${tagName}'. Error: ${e.message}`);\n }\n } else {\n processedValue = trimmedValue; // Use the trimmed version for further processing\n }\n }\n\n // 2. Initial check for null/undefined values\n if (processedValue === null || processedValue === undefined) {\n return REMOVE_EMPTY_VALUES ? '' : `${openTag}${closeTag}`;\n }\n\n // 3. Process based on type and calculate innerContent\n let innerContent = '';\n const nlArrayItemSep = generateNewlines(NEWLINES_ARRAY_ITEMS);\n const nlObjectPropSep = generateNewlines(NEWLINES_OBJECT_PROPERTIES);\n\n if (Array.isArray(processedValue)) {\n if (processedValue.length === 0) {\n return REMOVE_EMPTY_VALUES ? '' : `${openTag}${closeTag}`;\n }\n\n const elementsAfterDateFormatting = processedValue.map(el => formatIfDate(el));\n const allPrimitives = elementsAfterDateFormatting.every(el => isPrimitive(el));\n\n if (allPrimitives) { // Array of primitives -> join\n const delimiter = findArrayDelimiter(elementsAfterDateFormatting);\n const joinedString = elementsAfterDateFormatting\n .map(el => String(el === null || el === undefined ? '' : el))\n .join(delimiter)\n .trim();\n if (REMOVE_EMPTY_VALUES && joinedString === '') return '';\n innerContent = joinedString;\n } else { // Complex/mixed array -> iterate and join with configured separator\n const innerParts = [];\n processedValue.forEach((item, index) => {\n const itemXml = jsonToXmlRecursive(item, String(index)); // Use index as tag name for array items\n if (itemXml !== '') innerParts.push(itemXml);\n });\n if (REMOVE_EMPTY_VALUES && innerParts.length === 0) return '';\n innerContent = innerParts.join(nlArrayItemSep); // Uses array item separator\n }\n } else if (typeof processedValue === 'object') {\n if (Object.keys(processedValue).length === 0) {\n return REMOVE_EMPTY_VALUES ? '' : `${openTag}${closeTag}`;\n }\n // Object -> iterate and join with configured separator\n const innerParts = [];\n for (const key in processedValue) {\n if (Object.prototype.hasOwnProperty.call(processedValue, key)) {\n const propertyXml = jsonToXmlRecursive(processedValue[key], key);\n if (propertyXml !== '') innerParts.push(propertyXml);\n }\n }\n if (REMOVE_EMPTY_VALUES && innerParts.length === 0) return '';\n innerContent = innerParts.join(nlObjectPropSep); // Uses object property separator\n } else { // Primitive\n const finalValue = formatIfDate(processedValue);\n const stringValue = String(finalValue === null || finalValue === undefined ? '' : finalValue).trim();\n if (REMOVE_EMPTY_VALUES && stringValue === '') return '';\n innerContent = stringValue;\n }\n\n // 4. Construct the final XML for this value WITH configured newlines\n const nlWithin = generateNewlines(NEWLINES_WITHIN_TAGS);\n if (innerContent === '') {\n // If content is empty, no internal newlines\n return `${openTag}${closeTag}`;\n } else {\n // If there is content, add the configured internal newlines\n return `${openTag}${nlWithin}${innerContent}${nlWithin}${closeTag}`;\n }\n}\n\n// --- Main Conversion Logic ---\n\nlet finalXmlParts = [];\nconst nlTopLevelSep = generateNewlines(NEWLINES_TOP_LEVEL); // Separator for level 1\n\nif (inputJson && typeof inputJson === 'object' && !Array.isArray(inputJson)) {\n // If the input is a direct object, iterate its properties as top-level elements\n for (const topLevelKey in inputJson) {\n if (Object.prototype.hasOwnProperty.call(inputJson, topLevelKey)) {\n const part = jsonToXmlRecursive(inputJson[topLevelKey], topLevelKey);\n if (part !== '') finalXmlParts.push(part);\n }\n }\n} else if (inputJson !== null && inputJson !== undefined) {\n // If the input is a primitive or an array, wrap it in a <root> tag\n console.warn(\"Non-object input. Encapsulating in <root>.\");\n const part = jsonToXmlRecursive(inputJson, \"root\");\n if (part !== '') {\n finalXmlParts.push(part);\n } else {\n console.warn(\"Entire input is empty and omitted.\");\n }\n} else {\n console.warn(\"Empty/invalid JSON input.\");\n}\n\n// Join the top-level XML fragments with the configured separator\nconst finalXmlString = finalXmlParts.join(nlTopLevelSep);\n\n// --- Output Preparation ---\nreturn {\n xml: finalXmlString\n};"
},
"typeVersion": 2
},
{
"id": "8206f932-d658-41db-a8bc-50568d18f495",
"name": "JSON to XML",
"type": "n8n-nodes-base.executeWorkflowTrigger",
"position": [
-128,
48
],
"parameters": {
"inputSource": "passthrough"
},
"typeVersion": 1.1
},
{
"id": "9e3350b9-b6ad-4993-8cef-995ff73eb4cd",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-704,
-624
],
"parameters": {
"color": 7,
"width": 688,
"height": 824,
"content": "## JSON to XML Converter (Code Node)\n\nThis workflow utilizes a custom Code node to transform a JSON input into an XML structure. It offers flexibility in formatting and handling empty values.\n\n### Key Features:\n* **Recursive conversion** from JSON to XML.\n* **Array Handling:**\n * Primitive arrays are **joined with a safe delimiter** (e.g., `val1,val2`).\n * Complex arrays are converted into `<0>`, `<1>`, etc., tags.\n* **Date detection and formatting** (YYYY-MM-DD HH:mm:ss).\n* **Attempted parsing** of JSON/Array strings contained within values.\n* **Encapsulates non-object input** within a `<root>` tag.\n\n---\n\n### Input:\n\n* A single item (process each item in the subworkflow).\n\n### Output:\n\n* An object with an `xml` property containing the generated XML string, very useful for AI prompts.\n\n---\n\n### Automate your operations today\nYour time is valuable. Let us automate the boring stuff for you.\n\n**\ud83d\udc47 CHOOSE YOUR PATH:**\n\n[ **\u26a1\ufe0f I WANT A FREE AUDIT (2 min)** ](https://workflows.ac/audit?utm_source=n8n_template&utm_medium=workflow_note&utm_campaign=transform_json_to_xml_for_enhanced_ai_prompt_formatting&utm_content=5144)\n> *We've put our heart into this business evaluation machine.*\n\n[ **\ud83d\udca1 I HAVE A SPECIFIC REQUEST** ](https://workflows.ac/form?utm_source=n8n_template&utm_medium=workflow_note&utm_campaign=transform_json_to_xml_for_enhanced_ai_prompt_formatting&utm_content=5144)\n"
},
"typeVersion": 1
},
{
"id": "0e34e239-4490-44cc-8531-8869df671b90",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
32,
-480
],
"parameters": {
"color": 6,
"width": 400,
"height": 680,
"content": "### Configuration (within the code):\n\n* **`REMOVE_EMPTY_VALUES`** (boolean): Omits XML tags if their content is empty (default: `true`).\n* **Configurable Newlines** (`NEWLINES_TOP_LEVEL`, `NEWLINES_ARRAY_ITEMS`, `NEWLINES_OBJECT_PROPERTIES`, `NEWLINES_WITHIN_TAGS`): Controls indentation and readability of the generated XML.\n\n---\n\n### \u26a0\ufe0f IMPORTANT / NOTE \u26a0\ufe0f:\n\n* **No XML escaping for special characters** (e.g., <, >, &, ', \"). If your JSON contains these characters in values, they will be inserted as-is into the XML, which might render the XML invalid or vulnerable. A manual escaping step might be necessary if this is an issue.\n* This node is a custom script. Any modifications or maintenance must be done directly within the code."
},
"typeVersion": 1
},
{
"id": "442907fb-24f2-4f7b-9372-ad14206d4e20",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
480,
-944
],
"parameters": {
"color": 7,
"width": 540,
"height": 1136,
"content": "## Was this helpful? Let me know!\n[](https://api.ia2s.app/form/templates/academy)\n\nI really hope this template helped you. Your feedback is what helps me create better resources for the n8n community.\n\n### **Have Feedback, a Question, or a Project Idea?**\n\nI've streamlined the way we connect. It all starts with one simple form that takes less than 15 seconds.\n\n#### \u27a1\ufe0f **[Click here to go to the Contact Form](https://workflows.ac/form?utm_source=n8n_template&utm_medium=workflow_note&utm_campaign=transform_json_to_xml_for_enhanced_ai_prompt_formatting&utm_content=5144)**\n\nUse this single link for anything you need:\n\n* **Give Feedback:** Share your thoughts on this template, whether you found a typo, encountered an unexpected error, have a suggestion, or just want to say thanks!\n\n* **Automation Coaching:** Get personalized, one-on-one guidance to master n8n. We can work together to help you reach an expert level.\n\n* **Automation Consulting:** Have a complex business challenge or need custom workflows built from scratch? We offer a plug and play automation department for 8 to 88 people teams with unlimited automation requests.\n\n---\n\nHappy Automating!\nLucas Peyrin | [Workflows Accelerator](https://workflows.ac?utm_source=n8n_template&utm_medium=workflow_note&utm_campaign=transform_json_to_xml_for_enhanced_ai_prompt_formatting&utm_content=5144)"
},
"typeVersion": 1
}
],
"connections": {
"JSON to XML": {
"main": [
[
{
"node": "Convert JSON to XML",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This template provides a powerful and configurable utility to convert JSON data into a clean, well-structured XML format. It is designed for developers, data analysts, and n8n users who need to interface with legacy systems, generate structured reports, or prepare data for…
Source: https://n8n.io/workflows/5144/ — 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.
Back up n8n workflows to Google Drive automatically This workflow provides a robust solution for backing up your n8n workflows to Google Drive. It is designed to handle backups for multiple n8n instan
This workflow uses KlickTipp community nodes, available for self-hosted n8n instances only.
This workflow is a robust and forgiving JSON parser designed to handle malformed or "dirty" JSON strings often returned by AI models or scraped from web pages. It takes a text string as input and atte
This automation monitors X (formerly Twitter) search pages in real time and extracts high-signal posts that match your categories of interest. It’s ideal for community engagement, lead discovery, thou
This powerful automation helps you stay ahead of the competition by identifying sales opportunities in real-time. Here’s how it works: