Ever need a form from one Mautic instance copied to another Mautic instance? Me too. So I solved the problem; here’s the solution (in n8n).
Who uses this?
Anyone running multiple Mautic instances and wants to duplicate a form into a different Mautic instance.
How does it work?
In n8n (requires n8n, an entirely different platform from Mautic), a GET request is made to the API of the Mautic instance where the form already exists (source). Following, a Code node isolates all essential fields. The a POST request is made to the Mautic instance where you need the form duplicated (target).
Like this:
SOURCE MAUTIC Get Form → CODE NODE Build Request Body → TARGET MAUTIC Create New Form
How to use?
Get n8n.
Copy-paste this JSON into a blank n8n workflow:
{
"meta": {
"instanceId": "90f9a6ef38ec632934192a5de51518245cd649d4287258dedc9971969910cdb7"
},
"nodes": [
{
"parameters": {
"jsCode": "// For use in n8n Code Node\n// Crafted with special bazinga by Eric Knaus, Entrepositive, 1/1/2026\n// Input: GET /forms/:id response from Mautic (the whole JSON array or object)\n// Output: Cleaned POST-ready JSON for creating a duplicate form\n\n// ---- CONFIG: fields to remove at top level ----\nconst TOP_LEVEL_REMOVE = new Set([\n 'id',\n 'createdBy',\n 'createdByUser',\n 'modifiedBy',\n 'modifiedByUser',\n 'dateAdded',\n 'dateModified',\n 'category',\n 'publishUp',\n 'publishDown'\n]);\n\n// ---- CONFIG: fields to remove recursively everywhere ----\nconst GLOBAL_REMOVE = new Set([\n 'id',\n 'createdBy',\n 'createdByUser',\n 'modifiedBy',\n 'modifiedByUser',\n 'dateAdded',\n 'dateModified',\n 'leadField'\n]);\n\n// Remove empty properties entirely\nfunction cleanProperties(obj) {\n if (!obj || typeof obj !== 'object') return obj;\n\n if (Array.isArray(obj.properties) && obj.properties.length === 0) {\n delete obj.properties;\n }\n\n if (obj.properties && typeof obj.properties === 'object' && Object.keys(obj.properties).length === 0) {\n delete obj.properties;\n }\n\n return obj;\n}\n\n// Generic recursive cleaner\nfunction cleanObject(input, insideActions = false) {\n if (input === null || input === undefined) return input;\n\n // Handle arrays\n if (Array.isArray(input)) {\n return input\n .map(item => cleanObject(item, insideActions))\n .filter(item => item !== undefined);\n }\n\n // Handle objects\n if (typeof input === 'object') {\n const output = {};\n\n for (const key of Object.keys(input)) {\n const value = input[key];\n\n // Remove top-level form keys\n if (!insideActions && this.isTopLevel && TOP_LEVEL_REMOVE.has(key)) {\n continue;\n }\n\n // Strip IDs and metadata everywhere\n if (GLOBAL_REMOVE.has(key)) {\n continue;\n }\n\n // Remove empty property arrays\n if (key === 'properties' && Array.isArray(value) && value.length === 0) {\n continue;\n }\n\n // Recursively clean\n const cleaned = cleanObject.call(\n { isTopLevel: false },\n value,\n insideActions || key === 'actions'\n );\n\n // Skip empty objects\n if (\n cleaned &&\n typeof cleaned === 'object' &&\n !Array.isArray(cleaned) &&\n Object.keys(cleaned).length === 0\n ) {\n continue;\n }\n\n output[key] = cleaned;\n }\n\n return output;\n }\n\n return input;\n}\n\n// ------------------------------------------------------------\n// MAIN EXECUTION\n// ------------------------------------------------------------\nlet raw = items[0].json;\n\n// Mautic GET sometimes wraps as [{ form: {...} }]\nif (Array.isArray(raw) && raw.length === 1 && raw[0].form) {\n raw = raw[0].form;\n} else if (raw.form) {\n raw = raw.form;\n}\n\n// Clean the top-level object\nconst cleanedForm = cleanObject.call({ isTopLevel: true }, raw);\n\n// Result becomes the only output item\nreturn [{ json: cleanedForm }];"
},
"id": "48d0a612-dac0-40fc-ad67-6ed23d2ee288",
"name": "Code",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
920,
800
]
},
{
"parameters": {
"url": "https://SOURCEMAUTIC.COM/api/forms/[id]",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "mauticOAuth2Api",
"options": {}
},
"id": "9d03aa10-bc4a-4c9a-9556-de11c7d1772b",
"name": "GET the Form",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
720,
800
]
},
{
"parameters": {
"method": "POST",
"url": "https://TARGETMAUTIC.COM/api/forms/new",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "mauticOAuth2Api",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ $json }}",
"options": {}
},
"id": "63896e66-cfcf-4d98-bdfd-bfa236324559",
"name": "POST the Form",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1120,
800
]
}
],
"connections": {
"Code": {
"main": [
[
{
"node": "POST the Form",
"type": "main",
"index": 0
}
]
]
},
"GET the Form": {
"main": [
[
{
"node": "Code",
"type": "main",
"index": 0
}
]
]
}
},
"pinData": {}
}
Replace URLs for the GET and POST requests.
Fill in your Mautic creds for both API requests.
EP