This workflow corresponds to n8n.io template #8042 — 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 →
{
"id": "lqAMFCGhYpl6i0Kg",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Client-Organized Workflow Backup (n8n + GitLab)",
"tags": [
{
"id": "U9GFvr98FHfV6LSA",
"name": "backup-workflows",
"createdAt": "2025-08-20T16:32:01.267Z",
"updatedAt": "2025-08-21T19:09:58.119Z"
}
],
"nodes": [
{
"id": "2aaa2725-24b6-46bb-9f9e-153049095183",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"notes": "Manual trigger for testing the workflow execution.",
"position": [
-1136,
-128
],
"parameters": {},
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "a6a535bb-a39c-43e7-8121-16187772dacd",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"notes": "Runs the workflow daily at 03:00 (server time).",
"position": [
-1136,
64
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 3
}
]
}
},
"notesInFlow": true,
"typeVersion": 1.2
},
{
"id": "da551fd3-0e10-47f8-8325-5ca0b7bd375d",
"name": "Prepare Workflow JSON for UI-Compatible Export",
"type": "n8n-nodes-base.code",
"notes": "Cleans and normalizes workflow JSON to match n8n export format (only required fields).",
"position": [
1056,
32
],
"parameters": {
"jsCode": "// ---------------------------------------------------------------------------\n// n8n Code Node - Prepare Workflow JSON for UI-Compatible Export\n// ---------------------------------------------------------------------------\n//\n// GOAL: Produce an output *identical* to the workflow JSON downloaded from\n// the n8n UI \u2192 Same fields, same structure, same order.\n// ---------------------------------------------------------------------------\n\nreturn $input.all().map(item => {\n const w = item.json;\n\n // Keep exactly the fields that appear in a native n8n export\n // And ensure they are in the correct order\n const cleaned = {\n name: w.name, // Workflow name\n nodes: w.nodes || [], // Workflow nodes\n pinData: w.pinData || {}, // Pinned node data (empty if not set)\n connections: w.connections || {}, // Workflow connections\n active: w.active ?? false, // Default false like exported UI\n settings: w.settings || {}, // Workflow-level settings\n versionId: w.versionId || \"\", // Keep version ID if present\n meta: w.meta || {}, // Additional meta info\n id: w.id, // Workflow unique ID\n tags: w.tags || [] // Array of tags\n };\n\n // Return the cleaned JSON, ready for export or GitLab storage\n return {\n json: cleaned\n };\n});"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "092317f4-3c1e-4c9b-af35-fdeabc9e578f",
"name": "Clean & Normalize Workflow Name",
"type": "n8n-nodes-base.code",
"notes": "Cleans and normalizes workflow name: applies client tag (uppercase) or removes it if missing.",
"position": [
800,
32
],
"parameters": {
"jsCode": "// ---------------------------------------------------------------------------\n// n8n Code Node - Clean & Normalize Workflow Name\n// ---------------------------------------------------------------------------\n//\n// GOAL: Standardize workflow names by properly formatting [client] tags.\n//\n// - Detects any existing [client] tags in the name.\n// - If a client value exists \u2192 Normalize to `[client : NAME]`.\n// - If the tag exists but is empty \u2192 Remove it completely.\n// - If no tag is present \u2192 Leave the name unchanged.\n//\n// Output structure matches the original item, only the `name` is updated.\n// ---------------------------------------------------------------------------\n\nreturn $input.all().map(item => {\n const w = item.json;\n\n // Regex to match patterns like:\n // [clientXYZ], [client: XYZ], [client : xyz], [client xyz]\n // [ Client: XYZ] with optional space after [\n // Also matches empty tags: [client], [client:], [client : ]\n const clientTag = /\\[\\s*client\\s*:?\\s*([^\\]\\r\\n]*)\\]/i;\n\n let name = String(w.name || \"\").trim();\n const match = name.match(clientTag);\n\n if (match) {\n // Extract raw client value from the tag\n const rawClient = match[1] ? match[1].trim() : \"\";\n\n if (rawClient) {\n // Normalize client name to uppercase\n const clientName = rawClient.toUpperCase();\n\n // Remove the original tag completely\n name = name.replace(clientTag, \"\").trim();\n\n // Rebuild the name with the normalized format\n name = `[client : ${clientName}] ${name}`;\n } else {\n // If tag exists but has no value \u2192 remove it entirely\n name = name.replace(clientTag, \"\").trim();\n }\n }\n\n // Return the updated workflow JSON (only name is modified)\n return {\n json: {\n ...w,\n name\n }\n };\n});"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "4bed4024-392c-4027-907c-0f534dea3466",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
656,
-640
],
"parameters": {
"width": 912,
"height": 912,
"content": "\n\n## \ud83d\udfe8 Prepare Workflow Data \ud83d\udee0\ufe0f\n\n### \ud83c\udfaf Goal\nClean and prepare workflow data for a consistent and stable GitLab export. \nThe GitLab file path is always based on the workflow **ID** (rename-proof). \nThe normalized name is only used for readability and commit messages.\n\n### \ud83d\udd17 Nodes\n- **Normalize Workflow Name** \u2192 standardizes the workflow name and `[client: ...]` tag for readability and logging. \n- **Prepare Workflow JSON for UI-Compatible Export** \u2192 outputs the workflow JSON in the same format as an n8n UI export (includes `id`, `name`, nodes, tags, etc.). \n- **Prepare GitLab File Path** \u2192 generates the final storage path for GitLab backups using the workflow **ID** (e.g. `workflow_definitions/<workflowId>.json`).\n\n### \u2705 Best Practices\n- \ud83d\udcc2 Always use the workflow ID for file naming to ensure stability, even if names change. \n- \ud83d\udc40 Keep JSON human-readable and re-import friendly. \n\n### \ud83d\udce4 Key Outputs\n- `id` (from exported workflow JSON) \n- `gitlab_file_path` (constructed path based on workflow ID) \n- Full workflow JSON ready for GitLab commit\n"
},
"typeVersion": 1
},
{
"id": "91eb911b-8a50-4db7-b987-c79ee6cc6e67",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-240,
-640
],
"parameters": {
"color": 4,
"width": 806,
"height": 912,
"content": "\n\n## \ud83d\udfe9 Gather Workflows from n8n \ud83d\udcc2\n\n### \ud83c\udfaf Goal \nIdentify which workflows should be backed up, using tag-based filtering directly in the API call.\n\n### \ud83d\udd17 Node \nFetch Workflows from n8n \u2192 retrieves only workflows tagged with backup-workflows (filter applied via Tags).\n\n### \u2705 Best practices \n\ud83c\udff7\ufe0f Tag all critical workflows with backup-workflows to include them in backups. \n\ud83d\udd0d Audit tags regularly for naming consistency and completeness. \n\n### \ud83d\udce4 Key outputs \nFiltered list of workflows selected for backup.\n"
},
"typeVersion": 1
},
{
"id": "33a61637-4c62-4349-8a55-115ca7b5adbc",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1264,
-640
],
"parameters": {
"color": 6,
"width": 934,
"height": 912,
"content": "\n\n## \ud83d\udfea Start / Trigger & Configure \u26a1\n\n### \ud83c\udfaf Goal\nStart the backup manually or via CRON schedule and initialize GitLab + execution variables. \n\n### \ud83d\udd17 Nodes\n- **When clicking 'Execute workflow'** \u2192 manual run. \n- **Schedule Trigger** \u2192 scheduled execution (e.g. `0 3 * * *`). \n- **Set Global GitLab Variables** \u2192 defines owner, project, storage path, tag filter, execution type & timestamp. \n\n### \u2705 Best practices\n- \ud83e\uddea Run manually once after workflow changes. \n- \ud83d\udd10 Keep GitLab credentials encrypted in n8n. \n\n### \ud83d\udce4 Key outputs\n- `execution_type` = `Manual` | `Scheduled` \n- `execution_time` = ISO timestamp \n- [ISO timestamp](https://crontab.guru/examples.html)\n"
},
"typeVersion": 1
},
{
"id": "a8d0f8cc-70c5-4713-bb03-8d219feedbae",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
1664,
-1648
],
"parameters": {
"color": 4,
"width": 896,
"height": 1920,
"content": "\n\n## \ud83d\udfe9 GitLab File Management \ud83d\udcd1\n\n### \ud83c\udfaf Goal\nCompare the current workflow version with the GitLab repository and decide whether to create, update, or skip. \nThe GitLab file path is always derived from the **workflow ID**, ensuring stability even if names change.\n\n### \ud83d\udd17 Nodes\n- **Fetch Existing File in GitLab** \u2192 tries to read the current file. \n - If it exists \u2192 passes to **Compare Workflow with GitLab Version**. \n - If it does not exist \u2192 uses the *error output* (On Error \u2192 Continue) and passes directly to **Create New File in GitLab**. \n- **Compare Workflow with GitLab Version** \u2192 checks for JSON content differences. \n- **Create New File in GitLab** \u2192 creates the file if not found. \n- **Update Existing File in GitLab** \u2192 updates only if JSON content differs.\n\n### \u2705 Best practices\n- \ud83d\udcdd Use one commit per workflow for better traceability. \n- \ud83d\udeab Avoid unnecessary commits with strict JSON comparison. \n- \ud83d\udd12 Always rely on the workflow **ID** for file paths, never on the workflow name.\n\n### \ud83d\udce4 Key outputs\n- Action performed: `created` | `updated` | `unchanged`\n"
},
"typeVersion": 1
},
{
"id": "a0718c04-0981-472d-86f1-d96412dc4b79",
"name": "Fetch Workflows from n8n",
"type": "n8n-nodes-base.n8n",
"notes": "Fetches only workflows tagged \"backup-workflows\" via n8n API.",
"position": [
112,
-32
],
"parameters": {
"filters": {
"tags": "={{ $json.tag_backup }}"
},
"requestOptions": {}
},
"credentials": {
"n8nApi": {
"name": "<your credential>"
}
},
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "79f360ed-86cc-40fe-a105-b3ef23120e97",
"name": "Fetch Existing File from GitLab",
"type": "n8n-nodes-base.gitlab",
"notes": "Fetches the existing workflow backup file from GitLab.",
"onError": "continueErrorOutput",
"position": [
1824,
-32
],
"parameters": {
"owner": "={{ $('Set Global GitLab Variables').item.json.gitlab_owner }}",
"filePath": "={{ $json.gitlab_file_path }}",
"resource": "file",
"operation": "get",
"repository": "={{ $('Set Global GitLab Variables').item.json.gitlab_project }}",
"asBinaryProperty": false,
"additionalParameters": {
"reference": "={{ $('Set Global GitLab Variables').item.json.gitlab_branch }}"
}
},
"credentials": {
"gitlabApi": {
"name": "<your credential>"
}
},
"executeOnce": false,
"notesInFlow": true,
"retryOnFail": false,
"typeVersion": 1,
"alwaysOutputData": false
},
{
"id": "15cd412a-dd8f-4b71-ab54-849a6a535af8",
"name": "Update Existing File in GitLab",
"type": "n8n-nodes-base.gitlab",
"notes": "Updates the existing workflow backup file in GitLab with the latest JSON export.",
"position": [
2320,
-208
],
"parameters": {
"owner": "={{ $('Set Global GitLab Variables').item.json.gitlab_owner }}",
"branch": "={{ $(\"Set Global GitLab Variables\").item.json.gitlab_branch }}",
"filePath": "={{ $json.file_path || $(\"Prepare GitLab File Path\").item.json.gitlab_file_path }}",
"resource": "file",
"operation": "edit",
"repository": "={{ $('Set Global GitLab Variables').item.json.gitlab_project }}",
"fileContent": "={{ JSON.stringify($(\"Prepare Workflow JSON for UI-Compatible Export\").item.json, null, 2) }}",
"commitMessage": "={{ \"Update backup for workflow: \" \n + $(\"Clean & Normalize Workflow Name\").item.json.name \n + \" (\" \n + ($json.file_path || $(\"Prepare GitLab File Path\").item.json.gitlab_file_path) \n + \")\" \n}}"
},
"credentials": {
"gitlabApi": {
"name": "<your credential>"
}
},
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "da9f550c-bd0f-4bc2-b221-cab11a8d7be6",
"name": "Create New File in GitLab",
"type": "n8n-nodes-base.gitlab",
"notes": "Creates a new workflow backup file in GitLab if it does not already exist.",
"position": [
2096,
64
],
"parameters": {
"owner": "={{ $('Set Global GitLab Variables').item.json.gitlab_owner }}",
"branch": "={{ $('Set Global GitLab Variables').item.json.gitlab_branch }}",
"filePath": "={{ $json.gitlab_file_path }}",
"resource": "file",
"repository": "={{ $('Set Global GitLab Variables').item.json.gitlab_project }}",
"fileContent": "={{ JSON.stringify($(\"Prepare Workflow JSON for UI-Compatible Export\").item.json, null, 2) }}",
"commitMessage": "={{ \"Add backup for workflow: \" \n + $(\"Clean & Normalize Workflow Name\").item.json.name \n + \" (\" \n + ($json.gitlab_file_path || $(\"Prepare GitLab File Path\").item.json.gitlab_file_path) \n + \")\" }}"
},
"credentials": {
"gitlabApi": {
"name": "<your credential>"
}
},
"notesInFlow": true,
"typeVersion": 1
},
{
"id": "5ddc1294-9b87-42d8-93b8-f5e695dbf8bb",
"name": "Normalize Backup Output",
"type": "n8n-nodes-base.set",
"notes": "Normalizes backup output: adds GitLab path, branch, owner, project, execution type & timestamp.",
"position": [
3392,
-16
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "46c0d9f1-dfe1-4ada-b7e5-0148badac474",
"name": "status",
"type": "string",
"value": "={{ $json.status }}"
},
{
"id": "5810a928-6341-4600-8d8a-147c7b003c79",
"name": "workflow_name",
"type": "string",
"value": "={{ $(\"Prepare Workflow JSON for UI-Compatible Export\").item.json.name }}"
},
{
"id": "d25b1d07-589c-4b02-ae3e-90d3e292dec8",
"name": "file_path",
"type": "string",
"value": "={{ $(\"Prepare GitLab File Path\").item.json.gitlab_file_path }}"
},
{
"id": "226b5ec1-c8e1-43e9-b621-44e8e87328da",
"name": "branch",
"type": "string",
"value": "={{ $(\"Set Global GitLab Variables\").item.json.gitlab_branch }}"
},
{
"id": "596584fb-4c78-4863-909c-728409de7e48",
"name": "gitlab_owner",
"type": "string",
"value": "={{ $(\"Set Global GitLab Variables\").item.json.gitlab_owner }}"
},
{
"id": "c8db01a0-5ca5-4bff-be3f-fe9fe0f5fc7b",
"name": "gitlab_project",
"type": "string",
"value": "={{ $(\"Set Global GitLab Variables\").item.json.gitlab_project }}"
},
{
"id": "c8d1166c-c5b0-44a3-9740-6ae0c5b077b8",
"name": "execution_type",
"type": "string",
"value": "={{ $(\"Set Global GitLab Variables\").item.json.execution_type }}"
},
{
"id": "5ef4679e-0125-419b-b154-b2562198c616",
"name": "execution_time",
"type": "string",
"value": "={{ $now }}"
}
]
}
},
"notesInFlow": true,
"typeVersion": 3.4
},
{
"id": "9939f6ff-8915-47f3-8a87-6ff204fd2c0d",
"name": "Set Global GitLab Variables",
"type": "n8n-nodes-base.set",
"notes": "Defines global GitLab variables (owner, project, branch, paths, tags, execution type) for reuse across the workflow.",
"position": [
-640,
-32
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "d3a64cab-d823-4bfb-9ff8-3f00f1bd0942",
"name": "gitlab_owner",
"type": "string",
"value": "n8n-ainexusone"
},
{
"id": "c13bd+1234567890e-94394bd818d6",
"name": "gitlab_project",
"type": "string",
"value": "n8n_workflow_backups"
},
{
"id": "2c58ec25-1e33-406c-821a-62eff539f2db",
"name": "gitlab_workflow_path",
"type": "string",
"value": "workflow_definitions"
},
{
"id": "1517ca87-dba2-4411-af85-8d2c91e7aa42",
"name": "gitlab_branch",
"type": "string",
"value": "main"
},
{
"id": "39a0387d-8195-4c49-9831-c87f77758cdd",
"name": "tag_backup",
"type": "string",
"value": "backup-workflows"
},
{
"id": "3357bb93-0e45-4a8d-aced-ebe5b7e93ef8",
"name": "execution_type",
"type": "string",
"value": "={{ ( $('Schedule Trigger').isExecuted) ? 'Scheduled' : 'Manual' }}"
}
]
}
},
"notesInFlow": true,
"typeVersion": 3.4
},
{
"id": "2ea3ccb3-8123-4f49-82be-c4b90e0c5f20",
"name": "Prepare GitLab File Path",
"type": "n8n-nodes-base.code",
"notes": "Builds a normalized GitLab file path for the workflow backup (workflowId + .json)",
"position": [
1312,
32
],
"parameters": {
"jsCode": "// ---------------------------------------------------------------------------\n// n8n Code Node - Prepare GitLab File Path (Dedicated Node)\n// ---------------------------------------------------------------------------\n//\n// GOAL: Generate the GitLab storage path for each workflow without polluting\n// the workflow JSON that will be versioned. File name is fixed on ID\n// to avoid duplication when the workflow name changes.\n// ---------------------------------------------------------------------------\n\nreturn $input.all().map(item => {\n const w = item.json; // Current workflow data\n\n // --------------------------------------------------------\n // Helper: normalize accents & slugify safely\n // --------------------------------------------------------\n const toSlug = (s) => String(s || '')\n .normalize('NFKD') // decompose accented characters\n .replace(/[\\u0300-\\u036f]/g, '') // remove diacritics\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-') // replace invalid chars with hyphens\n .replace(/^-+|-+$/g, ''); // trim hyphens at start/end\n\n // --------------------------------------------------------\n // 1. Extract and normalize the client slug if tag exists\n // --------------------------------------------------------\n const clientTag = /\\[client\\s*:?\\s*([^\\]\\r\\n]*)\\]/i;\n const match = (w.name || '').match(clientTag);\n\n let clientSlug = \"unassigned\"; // Default if no [client: ...] tag is found\n if (match && match[1].trim()) {\n clientSlug = toSlug(match[1].trim()) || \"unassigned\";\n }\n\n // --------------------------------------------------------\n // 2. Build the GitLab storage path (ID-based, rename-proof)\n // --------------------------------------------------------\n const basePath = $('Set Global GitLab Variables').first().json.gitlab_workflow_path; // From declared variables\n const filePath = `${basePath}/${clientSlug}/${w.id}.json`;\n\n // --------------------------------------------------------\n // 3. Return ONLY the GitLab file path\n // --------------------------------------------------------\n return {\n json: {\n gitlab_file_path: filePath\n }\n };\n});\n"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "9f4fd524-5c9b-4ba6-bef2-a6da86f64c7d",
"name": "Compare Workflow with GitLab Version",
"type": "n8n-nodes-base.if",
"notes": "Compares exported workflow JSON with the GitLab version to detect changes.",
"position": [
2096,
-128
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "2e7b9fc6-cf3e-4f3c-b8be-a55221f5d1f4",
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ JSON.stringify($(\"Prepare Workflow JSON for UI-Compatible Export\").item.json) }}",
"rightValue": "={{ JSON.stringify(JSON.parse($json.content.base64Decode().trim())) }}"
}
]
}
},
"notesInFlow": true,
"typeVersion": 2.2
},
{
"id": "71fc8901-e3ea-4540-9384-51525834964b",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
2656,
-1648
],
"parameters": {
"color": 5,
"width": 1280,
"height": 1920,
"content": "\n\n## \ud83d\udfe6 Output & Logging \ud83d\udcca\n\n### \ud83c\udfaf Goal\nStandardize results for reporting and monitoring. \n\n### \ud83d\udd17 Nodes\n- **Mark as Created** \u2192 enriches each workflow output with `status = created`. \n- **Mark as Updated** \u2192 enriches each workflow output with `status = updated`. \n- **Mark as Unchanged** \u2192 enriches each workflow output with `status = unchanged`. \n- **Merge Backup Results** \u2192 combines all outputs (`created`, `updated`, `unchanged`) into a single flow. \n- **Normalize Backup Output** \u2192 consolidates merged data into a standardized schema for logging and reporting. \n- **Summarize Backup Results** \u2192 aggregates counts per status and adds execution context (manual/scheduled + timestamp). \n\n### \u2705 Best practices\n- \ud83d\udce2 Use consistent logging for dashboards, Slack alerts, or monitoring systems. \n- \u2705 Ensure all cases (`created`, `updated`, `unchanged`) are tracked. \n- \ud83e\udde9 Keep outputs normalized to simplify downstream processing. \n- \ud83d\udcca Provide a final recap (`created`, `updated`, `unchanged`, `total`) for monitoring and auditing. \n\n### \ud83d\udce4 Key outputs\n- `status` = `created` | `updated` | `unchanged` \n- `workflow_name` \n- `file_path` \n- `execution_type`, `execution_time` \n- `recap` = `{ created, updated, unchanged, total }`\n"
},
"typeVersion": 1
},
{
"id": "fcc49db9-286c-4f06-8e4e-dc1f11a0488c",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1264,
-1648
],
"parameters": {
"color": 3,
"width": 1328,
"height": 912,
"content": "\n\n# \ud83d\udcd8 n8n \u2192 GitLab Backup (with `[client]`) \u2014 Cheat Sheet\n\n## 1\ufe0f\u20e3 Purpose\n- Versioned history, centralized repo, internal vs client separation.\n\n## 2\ufe0f\u20e3 Client Management\n- Default path: `workflow_definitions/<file>.json` \n- If name contains `[client: Acme]` \u2192 `workflow_definitions/acme/<file>.json`\n\n## 3\ufe0f\u20e3 Setup\n- Create tag: **`backup-workflows`** \n- Tag target workflows \n- Triggers: \n - \u26a1 Manual \n - \u23f0 Schedule (daily 03:00) \u2014 or cron example: `30 21 * * 6` (Sat 21:30)\n\n## 4\ufe0f\u20e3 Initialization (Globals)\n- GitLab owner/project \n- Root path: `workflow_definitions/` \n- Tag filter: `backup-workflows` \n- `execution_type` (Manual | Scheduled), `execution_time` (ISO)\n\n## 5\ufe0f\u20e3 Selection\n- Fetch Workflows \u2192 retrieve only workflows tagged `backup-workflows`\n\n"
},
"typeVersion": 1
},
{
"id": "178c79f8-345e-4d33-a8aa-4eb31da592b7",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
64,
-1648
],
"parameters": {
"color": 3,
"width": 1504,
"height": 912,
"content": "\n\n## 6\ufe0f\u20e3 Name Normalization\n- Extract `[client: Name]` tag \u2192 route file to subfolder `<client>/` (or `unassigned/` if none). \n- File name is fixed to `<workflowId>.json` to preserve history across renames. \n- Generate a `display_slug` (optional, from the workflow name) for documentation or index files.\n\n## 7\ufe0f\u20e3 GitLab Verification\n- Check if file already exists in GitLab.\n - \ud83c\udd95 No \u2192 **Create** (status = `created`, via *Mark as Created*).\n - \ud83d\udd04 Yes + content differs \u2192 **Update** (status = `updated`, via *Mark as Updated*).\n - \u23ed\ufe0f Yes + unchanged \u2192 **Skip** (status = `unchanged`, via *Mark as Unchanged*).\n\n## 8\ufe0f\u20e3 Backup Actions\n- **Create New File(s)** \u2192 first commit of a workflow. \n- **Update Existing File(s)** \u2192 commit only if JSON content changed. \n- **Skip Unchanged File(s)** \u2192 prevent unnecessary commits. \n- Commit message: \"Add/Update backup for workflow: <name> (<path>)\".\n\n## 9\ufe0f\u20e3 Best Practices\n- \ud83d\udd10 Limit GitLab token scope (repo-only). \n- \ud83c\udff7\ufe0f Standardize client names (`acme`, not `Acme Corp` vs `ACME`). \n- \ud83d\udcca Add a final recap (`created`, `updated`, `unchanged`). \n- \ud83d\udee0\ufe0f Compare JSON **object-to-object** (avoid false positives due to indentation or line breaks). \n\n## \ud83d\udd1f Expected Results\n- Only workflows tagged with `backup-workflows` are auto-saved. \n- Internal workflows \u2192 `workflow_definitions/`. \n- Client workflows \u2192 `workflow_definitions/<client>/`. \n- Clean, structured Git history with meaningful commits and standardized statuses. "
},
"typeVersion": 1
},
{
"id": "75b2037f-bad5-4988-9f82-73665e4ae0eb",
"name": "Mark as Created",
"type": "n8n-nodes-base.set",
"notes": "Tags workflow as \"created\" (new file added in GitLab).",
"position": [
2928,
64
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "8e6cb925-589a-4daa-bf5c-73a51092ae9e",
"name": "status",
"type": "string",
"value": "created"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "e995304e-5611-4905-9a33-b6fdd70d3655",
"name": "Mark as Updated",
"type": "n8n-nodes-base.set",
"notes": "Tags workflow as \"updated\" after backup comparison.",
"position": [
2928,
-96
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "8e6cb925-589a-4daa-bf5c-73a51092ae9e",
"name": "status",
"type": "string",
"value": "updated"
}
]
}
},
"notesInFlow": true,
"typeVersion": 3.4
},
{
"id": "69533d20-2a48-4cb4-810d-e65c2a4aa71c",
"name": "Mark as Unchanged",
"type": "n8n-nodes-base.set",
"notes": "Tags workflow as \"unchanged\" (no differences found).",
"position": [
2768,
-16
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "8e6cb925-589a-4daa-bf5c-73a51092ae9e",
"name": "status",
"type": "string",
"value": "unchanged"
}
]
}
},
"notesInFlow": true,
"typeVersion": 3.4
},
{
"id": "7af2d653-0358-4b5a-847c-8b1851aa2721",
"name": "Summarize Backup Results",
"type": "n8n-nodes-base.code",
"notes": "Summarizes backup results: counts created/updated/unchanged workflows and adds execution metadata.",
"position": [
3712,
-16
],
"parameters": {
"jsCode": "// n8n Code Node \u2014 Summarize Backup Results\n// ----------------------------------------\n\nconst items = $input.all();\n\n// Init counters\nlet recap = {\n created: 0,\n updated: 0,\n unchanged: 0,\n total: items.length,\n};\n\nfor (const item of items) {\n const status = item.json.status;\n if (status && recap.hasOwnProperty(status)) {\n recap[status]++;\n }\n}\n\n// Build output\nreturn [\n {\n json: {\n execution_type: items[0]?.json.execution_type || \"unknown\",\n execution_time: items[0]?.json.execution_time || new Date().toISOString(),\n recap,\n },\n },\n];"
},
"notesInFlow": true,
"typeVersion": 2
},
{
"id": "90c21f27-3af6-416f-978e-2ff38c300c7a",
"name": "Merge",
"type": "n8n-nodes-base.merge",
"notes": "Merges outputs from \"Mark as Updated/Unchanged/Created\" into a single stream.\n",
"position": [
3168,
-32
],
"parameters": {
"numberInputs": 3
},
"notesInFlow": true,
"typeVersion": 3.2
}
],
"active": true,
"settings": {
"executionOrder": "v1"
},
"versionId": "35bc096b-29a7-461f-9bc7-23fd23a9aa5c",
"connections": {
"Merge": {
"main": [
[
{
"node": "Normalize Backup Output",
"type": "main",
"index": 0
}
]
]
},
"Mark as Created": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 2
}
]
]
},
"Mark as Updated": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Set Global GitLab Variables",
"type": "main",
"index": 0
}
]
]
},
"Mark as Unchanged": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"Normalize Backup Output": {
"main": [
[
{
"node": "Summarize Backup Results",
"type": "main",
"index": 0
}
]
]
},
"Fetch Workflows from n8n": {
"main": [
[
{
"node": "Clean & Normalize Workflow Name",
"type": "main",
"index": 0
}
]
]
},
"Prepare GitLab File Path": {
"main": [
[
{
"node": "Fetch Existing File from GitLab",
"type": "main",
"index": 0
}
]
]
},
"Create New File in GitLab": {
"main": [
[
{
"node": "Mark as Created",
"type": "main",
"index": 0
}
]
]
},
"Set Global GitLab Variables": {
"main": [
[
{
"node": "Fetch Workflows from n8n",
"type": "main",
"index": 0
}
]
]
},
"Update Existing File in GitLab": {
"main": [
[
{
"node": "Mark as Updated",
"type": "main",
"index": 0
}
]
]
},
"Clean & Normalize Workflow Name": {
"main": [
[
{
"node": "Prepare Workflow JSON for UI-Compatible Export",
"type": "main",
"index": 0
}
]
]
},
"Fetch Existing File from GitLab": {
"main": [
[
{
"node": "Compare Workflow with GitLab Version",
"type": "main",
"index": 0
}
],
[
{
"node": "Create New File in GitLab",
"type": "main",
"index": 0
}
]
]
},
"Compare Workflow with GitLab Version": {
"main": [
[
{
"node": "Update Existing File in GitLab",
"type": "main",
"index": 0
}
],
[
{
"node": "Mark as Unchanged",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "Set Global GitLab Variables",
"type": "main",
"index": 0
}
]
]
},
"Prepare Workflow JSON for UI-Compatible Export": {
"main": [
[
{
"node": "Prepare GitLab File Path",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
gitlabApin8nApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Triggers manually or on schedule (03:00 daily by default) Fetches workflows tagged via n8n API Normalizes workflow names and applies tag convention Prepares JSON in the same structure as an n8n UI export Checks GitLab repository: Create new file if missing Update file if content…
Source: https://n8n.io/workflows/8042/ — 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.
Gitlab Code. Uses manualTrigger, noOp, splitInBatches, gitlab. Event-driven trigger; 21 nodes.
This template is inspired by Save your workflows into a GitHub repository by hikerspath and Back Up Your n8n Workflows To Github by jon-n8n.
Gitlab Filter. Uses manualTrigger, n8n, gitlab, stickyNote. Event-driven trigger; 16 nodes.
Fetches workflow definitions from within n8n, selecting only the ones that have one or more (configurable) assigned tags and then: Derives a suitable backup filename by reducing the workflow name to a
This solution ensures the secure backup and version control of your self-hosted n8n workflows by storing them in a GitLab repository. It compares current workflows with their GitLab counterparts, upda