Skip to content

Automatically backup your n8n workflows in GitLab

January 25, 2023

Since I’ve started freelancing, I have a lot more repetitive tasks to do each week. As a lazy programmer, I looked for a flexible and easy tool to do this on my own.

At first, I’ve tested the famous no-code tools, Zapier and Make (ex Integromat), but those tools weren’t flexible enough and were a little bit limited in their first price range (workflows or executions number limit). I’ve finally given a try to n8n, an open-source low-code tool and distributed with a self-hosted version, and this one is very flexible and more developer friendly. Great, I’ve decided to put all my automation workflows on this self-hosted n8n instance, but I still need to secure them in case of fire. And this is where the magic comes up…

Can I create a workflow to save all my workflows? Absolutely! And jon-n8n already did it for GitHub and distribute it on n8n templates. I’ve used his workflow as a base to create one linked to GitLab Files repository API. It seems it’s not possible to share a workflow on n8n templates without a cloud instance, so I’m sharing it here.

To use it, save the following snippet in a file with .json extension and import it in a new blank workflow. Create a dedicated repository on GitLab or reuse an existing one, update “Globals” node information, add your nodes credentials (n8n and GitLab API) and launch your workflow.

{
  "active": true,
  "connections": {
    "n8n": {
      "main": [
        [
          {
            "node": "OneAtATime",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "same": {
      "main": [
        [
          {
            "node": "OneAtATime",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "isDiffOrNew",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Globals": {
      "main": [
        [
          {
            "node": "n8n",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OneAtATime": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          },
          {
            "node": "Add slug",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "isDiffOrNew": {
      "main": [
        [
          {
            "node": "gitlab_status",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Every 12 hours": {
      "main": [
        [
          {
            "node": "Globals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On clicking 'execute'": {
      "main": [
        [
          {
            "node": "Globals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GitLab": {
      "main": [
        [
          {
            "node": "Get file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add slug": {
      "main": [
        [
          {
            "node": "GitLab",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get file": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "new": {
      "main": [
        [
          {
            "node": "New file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "New file": {
      "main": [
        [
          {
            "node": "OneAtATime",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "different": {
      "main": [
        [
          {
            "node": "Edit file",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "gitlab_status": {
      "main": [
        [
          {
            "node": "same",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "different",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "new",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit file": {
      "main": [
        [
          {
            "node": "OneAtATime",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "createdAt": "2023-01-23T17:08:25.052Z",
  "dataLoaded": true,
  "id": 12,
  "name": "n8n => Gitlab (Backup)",
  "nodes": [
    {
      "parameters": {},
      "id": "af28f832-7867-4959-82d8-45e9790cfe10",
      "name": "On clicking 'execute'",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [835, 335],
      "typeVersion": 1
    },
    {
      "parameters": {},
      "id": "e91007ac-876b-4919-bc8b-a007edf0766f",
      "name": "same",
      "type": "n8n-nodes-base.noOp",
      "position": [3000, 240],
      "typeVersion": 1
    },
    {
      "parameters": {},
      "id": "123ca083-de40-40c5-8ea0-d17d3e6c3776",
      "name": "different",
      "type": "n8n-nodes-base.noOp",
      "position": [3000, 440],
      "typeVersion": 1
    },
    {
      "parameters": {},
      "id": "f798adff-42c7-400c-9f42-ca913d631ec2",
      "name": "new",
      "type": "n8n-nodes-base.noOp",
      "position": [3000, 620],
      "typeVersion": 1
    },
    {
      "parameters": {
        "batchSize": 1,
        "options": {}
      },
      "id": "4ffbe161-ac7b-4786-a06e-07679bc8ab6d",
      "name": "OneAtATime",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [1460, 460],
      "typeVersion": 1
    },
    {
      "parameters": {
        "values": {
          "string": [
            {
              "name": "repo.owner",
              "value": "lusimeon"
            },
            {
              "name": "repo.name",
              "value": "n8n-templates"
            },
            {
              "name": "repo.path",
              "value": "workflows/"
            },
            {
              "name": "repo.branch",
              "value": "main"
            }
          ]
        },
        "options": {}
      },
      "id": "3ca3580c-fa09-4669-8d9e-4ed5c0032d27",
      "name": "Globals",
      "type": "n8n-nodes-base.set",
      "position": [1020, 460],
      "typeVersion": 1
    },
    {
      "parameters": {
        "triggerTimes": {
          "item": [
            {
              "mode": "everyX",
              "value": 12
            }
          ]
        }
      },
      "id": "9815a578-3e63-4128-bea5-8301745606b7",
      "name": "Every 12 hours",
      "type": "n8n-nodes-base.cron",
      "position": [835, 595],
      "typeVersion": 1
    },
    {
      "parameters": {
        "content": "## Workflow Backups\nThis workflow will automatically backup your workflows to your Gitlab account every 12 hours.\n\n### Setup\nOpen Globals and update the values below\n**repo.owner:** This is your Gitlab username\n**repo.name:** This is the name of your repository\n**repo.path:** This is the folder to use within the repository, If it doesn't exist it will be created.\n**repo.branch:** This is the branch to use within the repository.\n\nIf your username was `n8n-io` and your repository was called `n8n-backups` and you wanted the workflows to go into a `workflows` folder on the `main` branch you would set:\n\nrepo.owner - n8n-io\nrepo.name - n8n-backups\nrepo.path - workflows\nrepo.branch - main",
        "height": 494.1787507012084,
        "width": 389.78906250000017
      },
      "id": "fbdcfdd9-4f07-48e7-8586-092d67c7fe59",
      "name": "Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [380, 240],
      "typeVersion": 1
    },
    {
      "parameters": {
        "content": "## Single Item Loop\nWe process each workflow item one at a time, We first check Gitlab to see if a file exists then we merge the Gitlab Data and the API item so we can check if the values match in the function node `isDiffOrNew` we then set a status of `same`, `different` or `new`",
        "height": 679.2241333333343,
        "width": 755.2349999999991
      },
      "id": "29078a78-b383-48da-be23-2d959fd82ada",
      "name": "Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [1440, 180],
      "typeVersion": 1
    },
    {
      "parameters": {
        "content": "## Save the data\nUsing the switch node we work out what to do based on the previous status,  If it is the same we do nothing, If it is different we update the file in Gitlab and if it is new we create a new file. After this is done we loop back to the Split in Batches node (OneAtAtTime) to start the process again.",
        "height": 740.3401851851838,
        "width": 1120.0120370370353
      },
      "id": "cc2c5992-4fec-4374-be41-a6f92149f3ba",
      "name": "Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [2280, 120],
      "typeVersion": 1
    },
    {
      "parameters": {
        "filters": {}
      },
      "id": "926f0d45-31e5-4322-a9d8-36b8f8020076",
      "name": "n8n",
      "type": "n8n-nodes-base.n8n",
      "position": [1220, 460],
      "typeVersion": 1
    },
    {
      "parameters": {
        "jsCode": "// File Returned with Content\nif ($input.all()[0] && Object.keys($input.all()[0].json).includes(\"content\")) {\n  // Get JSON Objects\n  var origWorkflow = JSON.parse(Buffer.from($input.all()[0].json.content, 'base64'));\n  var n8nWorkflow = $input.all()[1].json;\n  \n  // Order JSON Objects\n  var orderedOriginal = {}\n  var orderedActual = {}\n  \n  Object.keys(origWorkflow).sort().forEach(function(key) {\n    orderedOriginal[key] = origWorkflow[key];\n  });\n  \n  Object.keys(n8nWorkflow).sort().forEach(function(key) {\n    orderedActual[key] = n8nWorkflow[key];\n  });\n\n  // Determine Difference\n  if ( JSON.stringify(orderedOriginal) === JSON.stringify(orderedActual) ) {\n    $input.all()[0].json.gitlab_status = \"same\";\n    $input.all()[0].json.content_decoded = orderedOriginal;\n  } else {\n    $input.all()[0].json.gitlab_status = \"different\";\n    $input.all()[0].json.content_decoded = orderedOriginal;\n    $input.all()[0].json.n8n_data_stringy = JSON.stringify(orderedActual, null, 2);\n  }\n// No File Returned / New Workflow\n} else {\n  // Order JSON Object\n  var n8nWorkflow = ($input.all()[1].json);\n  var orderedActual = {}\n  Object.keys(n8nWorkflow).sort().forEach(function(key) {\n    orderedActual[key] = n8nWorkflow[key];\n  });\n\n  // Proper Formatting\n  $input.all()[0].json.gitlab_status = \"new\";\n  $input.all()[0].json.n8n_data_stringy = JSON.stringify(orderedActual, null, 2);\n}\n\n// Return Items\nreturn $input.all();"
      },
      "id": "17871023-92b4-422d-863d-e5c271fe413b",
      "name": "isDiffOrNew",
      "type": "n8n-nodes-base.code",
      "position": [2520, 440],
      "typeVersion": 1
    },
    {
      "parameters": {
        "mode": "=append",
        "mergeByFields": {
          "values": [{}]
        },
        "options": {}
      },
      "id": "581f2f11-c853-41f2-bc02-c3d8dcc57f19",
      "name": "Merge",
      "type": "n8n-nodes-base.merge",
      "position": [2340, 440],
      "typeVersion": 2
    },
    {
      "parameters": {
        "resource": "repository",
        "operation": "get",
        "owner": "={{$node[\"Globals\"].json[\"repo\"][\"owner\"]}}",
        "repository": "={{$node[\"Globals\"].json[\"repo\"][\"name\"]}}"
      },
      "id": "9932c79b-50fb-48fc-8faa-31ace1437dc5",
      "name": "GitLab",
      "type": "n8n-nodes-base.gitlab",
      "typeVersion": 1,
      "position": [1760, 660]
    },
    {
      "parameters": {
        "jsCode": "function slugify(str) {\n\treturn str\n\t\t.toLowerCase()\n\t\t.trim()\n\t\t.replace(/[^\\w\\s-]/g, \"\")\n\t\t.replace(/[\\s_-]+/g, \"-\")\n\t\t.replace(/^-+|-+$/g, \"\");\n}\n\nfor (const item of $input.all()) {\n  item.json.slug = slugify(item.json.name);\n}\n\nreturn $input.all();"
      },
      "id": "ce011e67-35e8-4059-8e08-1eebc875c592",
      "name": "Add slug",
      "type": "n8n-nodes-base.code",
      "typeVersion": 1,
      "position": [1580, 660]
    },
    {
      "parameters": {
        "url": "=https://gitlab.com/api/v4/projects/{{ $json[\"id\"] }}/repository/files/{{ $node[\"Globals\"].json[\"repo\"][\"path\"].replace(\".\", \"%2E\").replace(\"/\", \"%2F\") }}{{                 ($node[\"Add slug\"].json[\"slug\"] + \".json\").replace(\".\", \"%2E\").replace(\"/\", \"%2F\")                 }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "gitlabApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "ref",
              "value": "={{ $node[\"Globals\"].json[\"repo\"][\"branch\"] }}"
            }
          ]
        },
        "options": {}
      },
      "id": "337a942b-7014-4e19-b1a0-f953680470dd",
      "name": "Get file",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [1940, 660],
      "alwaysOutputData": false,
      "continueOnFail": true
    },
    {
      "parameters": {
        "method": "POST",
        "url": "=https://gitlab.com/api/v4/projects/{{ $node[\"GitLab\"].json[\"id\"] }}/repository/files/{{ $node[\"Globals\"].json[\"repo\"][\"path\"].replace(\".\", \"%2E\").replace(\"/\", \"%2F\") }}{{                 ($node[\"Add slug\"].json[\"slug\"] + \".json\").replace(\".\", \"%2E\").replace(\"/\", \"%2F\")                 }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "gitlabApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "branch",
              "value": "={{ $node[\"Globals\"].json[\"repo\"][\"branch\"] }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "name": "content",
              "value": "={{$node[\"isDiffOrNew\"].json[\"n8n_data_stringy\"]}}"
            },
            {
              "name": "commit_message",
              "value": "=[N8N Backup] {{$node[\"OneAtATime\"].json[\"name\"]}} ({{$json[\"gitlab_status\"]}})"
            }
          ]
        },
        "options": {}
      },
      "id": "59ff37cd-7c6f-4762-bfc6-eaad45fa43de",
      "name": "New file",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [3220, 620]
    },
    {
      "parameters": {
        "dataType": "string",
        "value1": "={{$json[\"gitlab_status\"]}}",
        "rules": {
          "rules": [
            {
              "value2": "same"
            },
            {
              "value2": "different",
              "output": 1
            },
            {
              "value2": "new",
              "output": 2
            }
          ]
        }
      },
      "id": "6b5d5c0f-ae4b-4c11-a97b-840fcddc1178",
      "name": "gitlab_status",
      "type": "n8n-nodes-base.switch",
      "position": [2760, 440],
      "typeVersion": 1
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://gitlab.com/api/v4/projects/{{ $node[\"GitLab\"].json[\"id\"] }}/repository/files/{{ $node[\"Globals\"].json[\"repo\"][\"path\"].replace(\".\", \"%2E\").replace(\"/\", \"%2F\") }}{{                 ($node[\"Add slug\"].json[\"slug\"] + \".json\").replace(\".\", \"%2E\").replace(\"/\", \"%2F\")                 }}",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "gitlabApi",
        "sendQuery": true,
        "queryParameters": {
          "parameters": [
            {
              "name": "branch",
              "value": "={{ $node[\"Globals\"].json[\"repo\"][\"branch\"] }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "name": "content",
              "value": "={{$node[\"isDiffOrNew\"].json[\"n8n_data_stringy\"]}}"
            },
            {
              "name": "commit_message",
              "value": "=[N8N Backup] {{$node[\"OneAtATime\"].json[\"name\"]}} ({{$json[\"gitlab_status\"]}})"
            }
          ]
        },
        "options": {}
      },
      "id": "a6fd280a-2628-4f1d-a389-39eaff131c06",
      "name": "Edit file",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 3,
      "position": [3220, 440]
    }
  ],
  "pinData": {},
  "settings": {},
  "staticData": null,
  "tags": [],
  "updatedAt": "2023-01-24T09:05:53.000Z",
  "versionId": "44eb2ca9-23b0-45c5-a16b-f162ba26d5cd"
}