Mautic Form Duplicator

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

This is a really practical solution for anyone juggling multiple Mautic instances. Using n8n as the middle layer makes a lot of sense, especially since it lets you clean up all the Mautic-specific metadata before recreating the form. The workflow is easy to follow, and the Code node does the heavy lifting in a transparent way. It’s also nice that it avoids manual exports or fragile SQL hacks. For teams managing many environments, this feels like a clean, repeatable approach that could save a lot of time.