# Mautic Form Duplicator

**URL:** <https://forum.mautic.org/t/mautic-form-duplicator/37137>\
**Category:** Development\
**Tags:** development, forms\
**Created:** [January 2, 2026, 12:07am UTC](https://forum.mautic.org/t/mautic-form-duplicator/37137 "2026-01-02T00:07:47Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![entrepositive](https://sea2.discourse-cdn.com/flex020/user_avatar/forum.mautic.org/entrepositive/32/9645_2.png) [@entrepositive](https://forum.mautic.org/u/entrepositive)\
**Post date:** [January 2, 2026, 12:07am UTC](https://forum.mautic.org/t/mautic-form-duplicator/37137/1 "2026-01-02T00:07:47Z")

</div>

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](https://epl.ink/n8n-openai)).

## 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](https://epl.ink/n8n-openai), 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](https://epl.ink/n8n-openai).

Copy-paste this JSON into a blank n8n workflow:

```auto
{
  "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

---

<div class="post-metadata">

**Author:** ![sehagevez](https://sea2.discourse-cdn.com/flex020/user_avatar/forum.mautic.org/sehagevez/32/13298_2.png) [@sehagevez](https://forum.mautic.org/u/sehagevez)\
**Post date:** [January 21, 2026, 11:11pm UTC](https://forum.mautic.org/t/mautic-form-duplicator/37137/2 "2026-01-21T23:11:00Z")

</div>

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.
