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
wf_update_all_caches. Uses executeWorkflowTrigger. Event-driven trigger; 6 nodes.