
Customer-Aware Workflow Backup (n8n + GitLab)
Description
Categories
🚀 DevOps🤖 AI & Machine Learning
Nodes Used
n8n-nodes-base.ifn8n-nodes-base.n8nn8n-nodes-base.setn8n-nodes-base.setn8n-nodes-base.setn8n-nodes-base.setn8n-nodes-base.setn8n-nodes-base.coden8n-nodes-base.coden8n-nodes-base.code
PriceGratis
Views0
Last Updated11/28/2025
workflow.json
{
"id": "lqAMFCGhYpl6i0Kg",
"meta": {
"instanceId": "349b88c12bdc8f9e6e74ffcb3e46eb3d44a721bf354e94b04baaf67413d41cbb",
"templateCredsSetupCompleted": true
},
"name": "Customer-Aware 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 ‘Execute workflow’",
"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": [
-1120,
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 → 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 customer 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 [customer] tags.\n//\n// - Detects any existing [customer] tags in the name.\n// - If a customer value exists → Normalize to `[customer : NAME]`.\n// - If the tag exists but is empty → Remove it completely.\n// - If no tag is present → 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 // [customerXYZ], [customer: XYZ], [customer : xyz], [customer xyz]\n // Also matches empty tags: [customer], [customer:], [customer : ]\n const customerTag = /\\[customer\\s*:?\\s*([^\\]\\r\\n]*)\\]/i;\n\n let name = String(w.name || \"\").trim();\n const match = name.match(customerTag);\n\n if (match) {\n // Extract raw customer value from the tag\n const rawCustomer = match[1] ? match[1].trim() : \"\";\n\n if (rawCustomer) {\n // Normalize customer name to uppercase\n const customerName = rawCustomer.toUpperCase();\n\n // Remove the original tag completely\n name = name.replace(customerTag, \"\").trim();\n\n // Rebuild the name with the normalized format\n name = `[customer : ${customerName}] ${name}`;\n } else {\n // If tag exists but has no value → remove it entirely\n name = name.replace(customerTag, \"\").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## 🟨 Prepare Workflow Data 🛠️\n\n### 🎯 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### 🔗 Nodes\n- **Normalize Workflow Name** → standardizes the workflow name and `[customer: ...]` tag for readability and logging. \n- **Prepare Workflow JSON for UI-Compatible Export** → outputs the workflow JSON in the same format as an n8n UI export (includes `id`, `name`, nodes, tags, etc.). \n- **Prepare GitLab File Path** → generates the final storage path for GitLab backups using the workflow **ID** (e.g. `workflow_definitions/<workflowId>.json`).\n\n### ✅ Best practices\n- 📂 Always use the workflow ID for file naming to ensure stability, even if names change. \n- 👀 Keep JSON human-readable and re-import friendly. \n\n### 📤 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## 🟩 Gather Workflows from n8n 📂\n\n### 🎯 Goal \nIdentify which workflows should be backed up, using tag-based filtering directly in the API call.\n\n### 🔗 Node \nFetch Workflows from n8n → retrieves only workflows tagged with backup-workflows (filter applied via Tags).\n\n### ✅ Best practices \n🏷️ Tag all critical workflows with backup-workflows to include them in backups. \n🔍 Audit tags regularly for naming consistency and completeness. \n\n### 📤 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## 🟪 Start / Trigger & Configure ⚡\n\n### 🎯 Goal\nStart the backup manually or via CRON schedule and initialize GitLab + execution variables. \n\n### 🔗 Nodes\n- **When clicking 'Execute workflow'** → manual run. \n- **Schedule Trigger** → scheduled execution (e.g. `0 3 * * *`). \n- **Set Global GitLab Variables** → defines owner, project, storage path, tag filter, execution type & timestamp. \n\n### ✅ Best practices\n- 🧪 Run manually once after workflow changes. \n- 🔐 Keep GitLab credentials encrypted in n8n. \n\n### 📤 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## 🟩 GitLab File Management 📑\n\n### 🎯 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### 🔗 Nodes\n- **Fetch Existing File in GitLab** → tries to read the current file. \n - If it exists → passes to **Compare Workflow with GitLab Version**. \n - If it does not exist → uses the *error output* (On Error → Continue) and passes directly to **Create New File in GitLab**. \n- **Compare Workflow with GitLab Version** → checks for JSON content differences. \n- **Create New File in GitLab** → creates the file if not found. \n- **Update Existing File in GitLab** → updates only if JSON content differs.\n\n### ✅ Best practices\n- 📝 Use one commit per workflow for better traceability. \n- 🚫 Avoid unnecessary commits with strict JSON comparison. \n- 🔒 Always rely on the workflow **ID** for file paths, never on the workflow name.\n\n### 📤 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": {
"id": "NKNI2VBN5i19fFpj",
"name": "n8n-api-ainexusone"
}
},
"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": {
"id": "3xJQUM2wS07heXvB",
"name": "GitLab account"
}
},
"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": {
"id": "3xJQUM2wS07heXvB",
"name": "GitLab account"
}
},
"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": {
"id": "3xJQUM2wS07heXvB",
"name": "GitLab account"
}
},
"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": "c13bd008-2829-4950-862e-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 customer slug if tag exists\n // --------------------------------------------------------\n const customerTag = /\\[customer\\s*:?\\s*([^\\]\\r\\n]*)\\]/i;\n const match = (w.name || '').match(customerTag);\n\n let customerSlug = \"unassigned\"; // Default if no [customer: ...] tag is found\n if (match && match[1].trim()) {\n customerSlug = 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').item.json.gitlab_workflow_path; // From declared variables\n const filePath = `${basePath}/${customerSlug}/${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});"
},
"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## 🟦 Output & Logging 📊\n\n### 🎯 Goal\nStandardize results for reporting and monitoring. \n\n### 🔗 Nodes\n- **Mark as Created** → enriches each workflow output with `status = created`. \n- **Mark as Updated** → enriches each workflow output with `status = updated`. \n- **Mark as Unchanged** → enriches each workflow output with `status = unchanged`. \n- **Merge Backup Results** → combines all outputs (`created`, `updated`, `unchanged`) into a single flow. \n- **Normalize Backup Output** → consolidates merged data into a standardized schema for logging and reporting. \n- **Summarize Backup Results** → aggregates counts per status and adds execution context (manual/scheduled + timestamp). \n\n### ✅ Best practices\n- 📢 Use consistent logging for dashboards, Slack alerts, or monitoring systems. \n- ✅ Ensure all cases (`created`, `updated`, `unchanged`) are tracked. \n- 🧩 Keep outputs normalized to simplify downstream processing. \n- 📊 Provide a final recap (`created`, `updated`, `unchanged`, `total`) for monitoring and auditing. \n\n### 📤 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# 📘 n8n → GitLab Backup (with `[customer]`) — Cheat Sheet\n\n## 1️⃣ Purpose\n- Versioned history, centralized repo, internal vs customer separation.\n\n## 2️⃣ Customer Management\n- Default path: `workflow_definitions/<file>.json`\n- If name contains `[customer: Acme]` → `workflow_definitions/acme/<file>.json`\n\n## 3️⃣ Setup\n- Create tag: **`backup-workflows`**\n- Tag target workflows\n- Triggers:\n - ⚡ Manual\n - ⏰ Schedule (daily 03:00) — or cron example: `30 21 * * 6` (Sat 21:30)\n\n## 4️⃣ 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️⃣ Selection\n- Fetch Workflows → retrieve only workflows tagged backup-workflows\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️⃣ Name Normalization\n- Extract `[customer: Name]` tag → route file to subfolder `<customer>/` (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️⃣ GitLab Verification\n- Check if file already exists in GitLab.\n - 🆕 No → **Create** (status = `created`, via *Mark as Created*).\n - 🔄 Yes + content differs → **Update** (status = `updated`, via *Mark as Updated*).\n - ⏭️ Yes + unchanged → **Skip** (status = `unchanged`, via *Mark as Unchanged*).\n\n## 8️⃣ Backup Actions\n- **Create New File(s)** → first commit of a workflow. \n- **Update Existing File(s)** → commit only if JSON content changed. \n- **Skip Unchanged File(s)** → prevent unnecessary commits.\n- Commit message: \"Add/Update backup for workflow: <name> (<path>)\".\n\n## 9️⃣ Best Practices\n- 🔐 Limit GitLab token scope (repo-only). \n- 🏷️ Standardize customer names (`acme`, not `Acme Corp` vs `ACME`). \n- 📊 Add a final recap (`created`, `updated`, `unchanged`). \n- 🛠️ Compare JSON **object-to-object** (avoid false positives due to indentation or line breaks). \n\n## 🔟 Expected Results\n- Only workflows tagged with `backup-workflows` are auto-saved. \n- Internal workflows → `workflow_definitions/`. \n- Customer workflows → `workflow_definitions/<customer>/`. \n- Clean, structured Git history with meaningful commits and standardized statuses. \n"
},
"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 — 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,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "74c2b09d-c638-48fd-9db7-a9fcfc28d309",
"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 ‘Execute workflow’": {
"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
}
]
]
}
}
}