This workflow corresponds to n8n.io template #7304 — 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": "dmvTNU9rfNdgTeSp",
"name": "Advanced n8n Workflow Sync with GitHub",
"tags": [],
"nodes": [
{
"id": "09450fb6-e815-4c7b-88e9-a92da4d70d16",
"name": "Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-368,
912
],
"parameters": {
"options": {}
},
"executeOnce": false,
"typeVersion": 3
},
{
"id": "34869a4d-b687-44f7-81fd-5f71e8679a0e",
"name": "Update file content and commit",
"type": "n8n-nodes-base.github",
"position": [
1584,
1872
],
"parameters": {
"owner": {
"__rl": true,
"mode": "",
"value": "={{ $(\"Configuration\").item.json.repo.owner }}"
},
"filePath": "={{ $json.context.newFile.path }}",
"resource": "file",
"operation": "edit",
"repository": {
"__rl": true,
"mode": "",
"value": "={{ $(\"Configuration\").item.json.repo.name }}"
},
"fileContent": "={{ JSON.stringify($json.n8nWorkflowData.base64Decode().parseJson(), null, 2) }}",
"commitMessage": "=update: {{ $json.context.newFile.name }}"
},
"credentials": {
"githubApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "bbf77bc7-8c35-4177-be4f-44c91682fd1a",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-2912,
864
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "34d67ff9-e25a-42a9-b751-dbde37ed575b",
"name": "Get all workflows",
"type": "n8n-nodes-base.n8n",
"position": [
-1328,
848
],
"parameters": {
"filters": {},
"requestOptions": {}
},
"credentials": {
"n8nApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "98fc6907-d188-4f9a-b68e-44592bfac95b",
"name": "Encode N8N workflows",
"type": "n8n-nodes-base.code",
"position": [
-1104,
848
],
"parameters": {
"jsCode": "// Encode workflow data to base64 to prevent data pollution\nconst items = $input.all();\n\nfor (const item of items) {\n const originalWorkflow = item.json;\n\n item.json = {\n id: originalWorkflow.id,\n name: originalWorkflow.name,\n n8nWorkflowData: Buffer.from(JSON.stringify(originalWorkflow)).toString('base64')\n };\n}\n\nreturn items;"
},
"typeVersion": 2
},
{
"id": "3576b4b3-6692-4d08-9453-697d44ac0f56",
"name": "Decide changes",
"type": "n8n-nodes-base.code",
"position": [
-112,
928
],
"parameters": {
"jsCode": "// Helper function to ensure stable JSON serialization for reliable comparison.\nfunction sortKeysDeep(obj) {\n if (obj === null || typeof obj !== 'object') return obj;\n if (Array.isArray(obj)) return obj.map(sortKeysDeep);\n const out = {};\n Object.keys(obj).sort().forEach(k => { out[k] = sortKeysDeep(obj[k]); });\n return out;\n}\n\nconst items = $input.all();\nconst WORKFLOWS_DIR = $node[\"Configuration\"].json.repo.path;\n\nfor (const item of items) {\n const src = item.json || {};\n const flags = {\n fileExists: false,\n nameChanged: false,\n shouldCommit: false\n };\n // Initialize the context container\n item.json.context = {\n oldFile: { path: '', name: '' },\n newFile: { path: '', name: '' },\n operation: ''\n };\n const context = item.json.context;\n\n // 1. Determine if the file exists on GitHub.\n const hasGithub = typeof src.githubWorkflowData === 'string' && src.githubWorkflowData.length > 0;\n flags.fileExists = hasGithub;\n\n // 2. Extract the current workflow name from the N8N data.\n const currentName = src.name || '';\n context.newFile.name = currentName;\n\n // 3. Detect renames and set file paths.\n if (typeof src.filePath === 'string' && src.filePath.length > 0) {\n const parts = src.filePath.split('/');\n const filename = parts.pop() || '';\n const githubName = filename.endsWith('.json') ? filename.slice(0, -5) : filename;\n \n flags.nameChanged = githubName !== currentName;\n context.oldFile.path = src.filePath;\n context.oldFile.name = githubName;\n \n const dirPath = parts.join('/');\n context.newFile.path = `${dirPath}/${currentName}.json`;\n\n } else {\n flags.nameChanged = false;\n context.newFile.path = `${WORKFLOWS_DIR}/${currentName}.json`.replace(/\\/+/g, '/');\n }\n\n // 4. Perform a stable comparison to see if a commit is needed.\n try {\n if (flags.fileExists) {\n const n8nJsonStr = Buffer.from(src.n8nWorkflowData, 'base64').toString('utf8');\n const githubJsonStr = Buffer.from(src.githubWorkflowData, 'base64').toString('utf8');\n const n8nObj = JSON.parse(n8nJsonStr);\n const githubObj = JSON.parse(githubJsonStr);\n const stableN8nStr = JSON.stringify(sortKeysDeep(n8nObj));\n const stableGithubStr = JSON.stringify(sortKeysDeep(githubObj));\n flags.shouldCommit = stableN8nStr !== stableGithubStr;\n } else {\n flags.shouldCommit = true; // New file, always commit.\n }\n } catch (e) {\n flags.shouldCommit = true; // If parsing or comparison fails, better to commit.\n }\n\n // 5. Determine the final operation type.\n if (flags.nameChanged) {\n context.operation = 'rename';\n } else if (!flags.fileExists) {\n context.operation = 'create';\n } else if (flags.shouldCommit) {\n context.operation = 'update';\n } else {\n context.operation = 'skip';\n }\n}\n\nreturn items;"
},
"typeVersion": 2
},
{
"id": "1cc5a35b-8e52-46dd-ac69-03e320f66fc8",
"name": "Delete old file",
"type": "n8n-nodes-base.github",
"position": [
1744,
1408
],
"parameters": {
"owner": {
"__rl": true,
"mode": "",
"value": "={{ $(\"Configuration\").item.json.repo.owner }}"
},
"filePath": "={{ $json.context.oldFile.path }}",
"resource": "file",
"operation": "delete",
"repository": {
"__rl": true,
"mode": "",
"value": "={{ $(\"Configuration\").item.json.repo.name }}"
},
"commitMessage": "=rename: {{ $json.context.oldFile.name }} -> {{ $json.context.newFile.name }} (step 1/2: remove old)"
},
"credentials": {
"githubApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "c6464638-d7d4-4e36-b359-f70412db2bbb",
"name": "Create new file (rename)",
"type": "n8n-nodes-base.github",
"position": [
2144,
1408
],
"parameters": {
"owner": {
"__rl": true,
"mode": "",
"value": "={{ $(\"Configuration\").item.json.repo.owner }}"
},
"filePath": "={{ $json.context.newFile.path }}",
"resource": "file",
"repository": {
"__rl": true,
"mode": "",
"value": "={{ $(\"Configuration\").item.json.repo.name }}"
},
"fileContent": "={{ JSON.stringify($json.n8nWorkflowData.base64Decode().parseJson(), null, 2) }}",
"commitMessage": "=rename: {{ $json.context.oldFile.name }} -> {{ $json.context.newFile.name }} (step 2/2: create new)"
},
"credentials": {
"githubApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "b470dd47-762a-4c8a-90e0-e4f535e9b41e",
"name": "Merge after create (rename)",
"type": "n8n-nodes-base.merge",
"notes": "Keeps the original context intact across the GitHub step. Prevents losing flags and fields.",
"position": [
2384,
1392
],
"parameters": {
"mode": "combine",
"options": {
"clashHandling": {
"values": {
"resolveClash": "preferInput1"
}
}
},
"joinMode": "enrichInput1",
"fieldsToMatchString": "id"
},
"typeVersion": 3.2
},
{
"id": "be267762-77a4-4af5-ba26-7f3945d44b32",
"name": "Merge after update",
"type": "n8n-nodes-base.merge",
"notes": "Keeps the original context intact across the GitHub step. Prevents losing flags and fields.",
"position": [
1872,
1856
],
"parameters": {
"mode": "combine",
"options": {
"clashHandling": {
"values": {
"resolveClash": "preferInput1"
}
}
},
"joinMode": "enrichInput1",
"fieldsToMatchString": "id"
},
"typeVersion": 3.2
},
{
"id": "db1f95ae-42e5-4588-bb7d-5348011d9afe",
"name": "List files",
"type": "n8n-nodes-base.github",
"notes": "An edge case handling. Do not stop the whole workflow if there's no such folder.",
"onError": "continueErrorOutput",
"position": [
-1552,
1040
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $(\"Configuration\").item.json.repo.owner }}"
},
"filePath": "={{ $(\"Configuration\").item.json.repo.path }}",
"resource": "file",
"operation": "list",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $(\"Configuration\").item.json.repo.name }}"
}
},
"credentials": {
"githubApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "8c6f381b-caed-4267-abb4-6672c93f45e9",
"name": "Merge",
"type": "n8n-nodes-base.merge",
"position": [
-880,
928
],
"parameters": {
"mode": "combine",
"options": {
"clashHandling": {
"values": {
"resolveClash": "preferInput1"
}
}
},
"joinMode": "enrichInput1",
"fieldsToMatchString": "id"
},
"typeVersion": 3.2
},
{
"id": "8d8f0963-0b37-47d9-b263-ffc7da06d118",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
1680,
1168
],
"parameters": {
"color": 7,
"width": 880,
"height": 448,
"content": "## Rename a file (two-step)\n1. Delete the old filename\n1. Create a new filename"
},
"typeVersion": 1
},
{
"id": "371212bd-c19c-4908-b3cb-fe75034a95fb",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1616,
752
],
"parameters": {
"color": 7,
"width": 880,
"height": 448,
"content": "## Collect data\n- N8N workflows list\n- GitHub files list "
},
"typeVersion": 1
},
{
"id": "fb300a39-10b0-48cf-a793-a96e1ee3014f",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"disabled": true,
"position": [
-2672,
592
],
"parameters": {
"color": 4,
"width": 326,
"height": 448,
"content": "## Set parameters\n### GitHub\n- Repo owner\n- Repo name\n- Repo folder to store workflow backups\n### Reports\n- Telegram Chat ID to send notifications to\n- Do you need a report each time or only if something changed"
},
"typeVersion": 1
},
{
"id": "80dabce7-e36e-4ea7-bd7d-67fb3b3096f8",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"disabled": true,
"position": [
-3024,
592
],
"parameters": {
"color": 4,
"width": 326,
"height": 448,
"content": "## Tune the schedule\nYou could change the check interval here.\n\nDefault: every hour"
},
"typeVersion": 1
},
{
"id": "802494dc-8c76-4a34-97d3-c414cb04a179",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"disabled": true,
"position": [
176,
0
],
"parameters": {
"color": 7,
"width": 1174,
"height": 272,
"content": "## Execution report\nYou could send this report to Telegram. See parameters in `Configuration` node for details.\nIf you don't need this -- delete this part."
},
"typeVersion": 1
},
{
"id": "d0af7c02-a586-4993-8c4b-8fb18c3053fe",
"name": "Build summary arrays",
"type": "n8n-nodes-base.code",
"position": [
480,
112
],
"parameters": {
"jsCode": "// Aggregate arrays and flags for summary (no rendering)\n\nfunction normalizeName(item) {\n return String(\n item.json?.name || 'unknown'\n );\n}\n\nconst items = $input.all();\n\nconst buckets = { create: [], update: [], rename: [], skip: [] };\n\nfor (const it of items) {\n const name = normalizeName(it);\n const op = it.json?.context.operation;\n const oldName = it.json?.context.oldFile.name || name;\n const newName = it.json?.context.newFile.name || name;\n\n if (op === 'rename') buckets.rename.push(`${oldName} -> ${newName}`);\n else if (op === 'create') buckets.create.push(name);\n else if (op === 'update') buckets.update.push(name);\n else buckets.skip.push(name);\n}\n\nconst isAnythingChanged = buckets.create.length > 0 || buckets.update.length > 0 || buckets.rename.length > 0;\n\nreturn [{ json: {\n isAnythingChanged,\n created: buckets.create,\n updated: buckets.update,\n renamed: buckets.rename,\n skipped: buckets.skip\n} }];"
},
"typeVersion": 2
},
{
"id": "0c401771-b522-4091-a14f-aba4a06d6158",
"name": "Render summary",
"type": "n8n-nodes-base.code",
"notes": "## Connecting to a messenger\n\nUse {{$json.message}} as message text. 'isAnythingChanged' controls whether to send.",
"position": [
928,
112
],
"parameters": {
"jsCode": "// Helper function to escape text for Telegram's MarkdownV2 parser\nconst escapeMarkdownV2 = (str) => {\n // For the full list of characters, see https://core.telegram.org/bots/api#markdownv2-style\n return String(str).replace(/([_\\[\\]()~`>#+=|{}.!*-])/g, '\\\\$1');\n};\n\nconst all = $input.all();\nconst data = (all[0] && all[0].json) || {};\n\nconst config = $('Configuration').first().json;\nconst repoOwner = config.repo.owner;\nconst repoName = config.repo.name;\nconst repoPath = config.repo.path;\n\n// Construct repository URL using /blob/-/ for a branch-agnostic link\nconst repoUrl = `https://github.com/${repoOwner}/${repoName}/blob/-/${repoPath}`;\n\n// The link's *text* must be escaped, but the URL must not be.\nconst repoLinkText = escapeMarkdownV2(`${repoOwner}/${repoName}/${repoPath}`);\nconst repoLink = `[${repoLinkText}](${repoUrl})`;\n\nconst getList = key => (Array.isArray(data[key]) ? data[key] : []);\nconst sortAsc = (a, b) => String(a).localeCompare(String(b));\n\nconst sections = [\n { key: 'created', title: 'Created' },\n { key: 'updated', title: 'Updated' },\n { key: 'renamed', title: 'Renamed' },\n { key: 'skipped', title: 'Skipped (no changes)' },\n];\n\nconst summaryParts = [\n `created ${getList('created').length}`,\n `updated ${getList('updated').length}`,\n `renamed ${getList('renamed').length}`,\n `skipped ${getList('skipped').length}`\n];\n\n// Construct the final message with the new header format\nconst messageLines = [\n '*Backup N8N workflows to GitHub*', // Main title\n '', // Blank line for spacing\n `Repo: ${repoLink}`,\n `Totals: ${escapeMarkdownV2(summaryParts.join(', '))}`\n];\n\n// Append detailed lists as before\nfor (const { key, title } of sections) {\n const list = [...getList(key)].sort(sortAsc);\n if (list.length) {\n messageLines.push('', `*${escapeMarkdownV2(title)}:*`);\n for (const item of list) {\n let line;\n if (key === 'renamed') {\n const [oldName, newName] = item.split(' -> ');\n line = `\\`${oldName}.json\\` ${escapeMarkdownV2('->')} \\`${newName}.json\\``;\n } else {\n line = `\\`${item}.json\\``;\n }\n messageLines.push(line);\n }\n }\n}\n\nconst message = messageLines.join('\\n');\nreturn [{ json: { message } }];"
},
"notesInFlow": false,
"typeVersion": 2
},
{
"id": "fdb5e4f7-8aa1-4273-8776-ac0248b893e4",
"name": "Anything changed?",
"type": "n8n-nodes-base.if",
"position": [
704,
112
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "or",
"conditions": [
{
"id": "8148a3ce-16fe-4074-9f57-c49072be8a8f",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $node[\"Configuration\"].json.report.verbose }}",
"rightValue": ""
},
{
"id": "ee4ef204-341f-444a-a26a-299aa0cde573",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.isAnythingChanged }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
},
{
"id": "13ba1e58-2bac-44dc-9142-b61efef7c4e1",
"name": "Extract workflow parameters",
"type": "n8n-nodes-base.code",
"position": [
-1104,
1040
],
"parameters": {
"jsCode": "let items = $input.all();\n\nfor (let item of items) {\n try {\n const contentBase64 = item.json.content;\n const path = item.json.path;\n const sha = item.json.sha;\n\n // Decode and parse GitHub file content to extract name\n const content = Buffer.from(contentBase64, 'base64').toString('utf8');\n const workflow = JSON.parse(content);\n\n // Keep only the fields we need from GitHub side, store as base64\n item.json = {\n id: workflow.id,\n name: workflow.name,\n filePath: path,\n githubWorkflowData: contentBase64, // Store as base64 to match N8N side\n sha: sha\n };\n\n } catch (error) {\n // Non-JSON or invalid workflow file\n console.log(`Error parsing file ${item.json.path}: ${error.message}`);\n item.json = {\n id: null,\n name: null,\n filePath: item.json.path,\n error: error.message\n };\n }\n}\n\nreturn items;"
},
"typeVersion": 2
},
{
"id": "3464840a-599d-4432-aade-bfd5a2930461",
"name": "Get files",
"type": "n8n-nodes-base.github",
"notes": "An edge case handling. Do not stop the whole workflow if there's no such folder.",
"onError": "continueErrorOutput",
"position": [
-1328,
1040
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $(\"Configuration\").item.json.repo.owner }}"
},
"filePath": "={{ $json.path }}",
"resource": "file",
"operation": "get",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $(\"Configuration\").item.json.repo.name }}"
},
"asBinaryProperty": false,
"additionalParameters": {}
},
"credentials": {
"githubApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "d991e89f-d795-4559-9b3f-4ecce50723b5",
"name": "Is Telegram configured?",
"type": "n8n-nodes-base.if",
"position": [
256,
112
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "03191658-7c7b-4f85-a07b-35d9749d91f3",
"operator": {
"type": "number",
"operation": "exists",
"singleValue": true
},
"leftValue": "={{ $(\"Configuration\").item.json.report.tg.chatID }}",
"rightValue": 0
},
{
"id": "3277a7ef-0895-4fbf-beb2-432f54cc8efc",
"operator": {
"type": "number",
"operation": "notEquals"
},
"leftValue": "={{ $(\"Configuration\").item.json.report.tg.chatID }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.2
},
{
"id": "0705a90f-00e1-46e6-b5ef-4a4efe5255cd",
"name": "Send a message",
"type": "n8n-nodes-base.telegram",
"position": [
1152,
112
],
"parameters": {
"text": "={{ $json.message }}",
"chatId": "={{ $node[\"Configuration\"].json.report.tg.chatID }}",
"additionalFields": {
"parse_mode": "MarkdownV2",
"appendAttribution": false
}
},
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "fdef22d8-7960-48e2-a046-e69b3f48ce15",
"name": "Create new file",
"type": "n8n-nodes-base.github",
"position": [
1296,
2416
],
"parameters": {
"owner": {
"__rl": true,
"mode": "",
"value": "={{ $(\"Configuration\").item.json.repo.owner }}"
},
"filePath": "={{ $json.context.newFile.path }}",
"resource": "file",
"repository": {
"__rl": true,
"mode": "",
"value": "={{ $(\"Configuration\").item.json.repo.name }}"
},
"fileContent": "={{ JSON.stringify($json.n8nWorkflowData.base64Decode().parseJson(), null, 2) }}",
"commitMessage": "=create: {{ $json.context.newFile.name }}"
},
"credentials": {
"githubApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "669101cb-2d23-41c7-8e07-6e282b556cdd",
"name": "Merge after create",
"type": "n8n-nodes-base.merge",
"notes": "Keeps the original context intact across the GitHub step. Prevents losing flags and fields.",
"position": [
1584,
2400
],
"parameters": {
"mode": "combine",
"options": {
"clashHandling": {
"values": {
"resolveClash": "preferInput1"
}
}
},
"joinMode": "enrichInput1",
"fieldsToMatchString": "id"
},
"typeVersion": 3.2
},
{
"id": "c5d3b5e4-9c2a-4cf8-8bae-fca50b9f884c",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
1328,
1712
],
"parameters": {
"color": 7,
"width": 880,
"height": 448,
"content": "## Update an existing file"
},
"typeVersion": 1
},
{
"id": "d58b3267-9510-4abd-b4df-14f5cc19f984",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
1040,
2256
],
"parameters": {
"color": 7,
"width": 880,
"height": 448,
"content": "## Create a new file"
},
"typeVersion": 1
},
{
"id": "2ababf2c-5315-4d6a-9f09-b2efaef4ab2c",
"name": "No Operation, do nothing",
"type": "n8n-nodes-base.noOp",
"position": [
1056,
2976
],
"parameters": {},
"typeVersion": 1
},
{
"id": "7e069ed2-7303-4db5-b0b3-f18edbfb88da",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
640,
2800
],
"parameters": {
"color": 7,
"width": 880,
"height": 448,
"content": "## Nothing to do"
},
"typeVersion": 1
},
{
"id": "0ac9743d-298a-49d2-b391-4c34b9d5122b",
"name": "Stop and Error",
"type": "n8n-nodes-base.stopAndError",
"position": [
336,
3376
],
"parameters": {
"errorMessage": "=Invalid operation: \"{{ $json.context.operation }}\". You should look at the code in the \"Decide changes\" node."
},
"typeVersion": 1
},
{
"id": "f7a498d9-900f-41ab-9b5f-8e849b85c38c",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
-512,
752
],
"parameters": {
"color": 7,
"width": 880,
"height": 448,
"content": "## Controller"
},
"typeVersion": 1
},
{
"id": "4d426ef2-c4db-4690-bd23-48c374fcf0e1",
"name": "Router",
"type": "n8n-nodes-base.switch",
"position": [
128,
880
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "=rename",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "5af9aafc-3ee1-4855-89b2-b0ceb83b3169",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.context.operation }}",
"rightValue": "rename"
}
]
},
"renameOutput": true
},
{
"outputKey": "update",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "849881fc-2d0e-4154-b6c2-10ff6c2b5480",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.context.operation }}",
"rightValue": "update"
}
]
},
"renameOutput": true
},
{
"outputKey": "create",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "85f4cce5-476e-4970-82b4-0b04cc67870f",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.context.operation }}",
"rightValue": "create"
}
]
},
"renameOutput": true
},
{
"outputKey": "skip",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "51919025-e488-4557-9cd9-23f4be9bbf06",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.context.operation }}",
"rightValue": "skip"
}
]
},
"renameOutput": true
}
]
},
"options": {
"ignoreCase": true,
"fallbackOutput": "extra",
"renameFallbackOutput": "error"
}
},
"typeVersion": 3.2
},
{
"id": "921826cc-13da-4db8-8a18-fdffacbcf5bb",
"name": "Merge after delete (rename)",
"type": "n8n-nodes-base.merge",
"notes": "Keeps the original context intact across the GitHub step. Prevents losing flags and fields.",
"position": [
1968,
1248
],
"parameters": {
"mode": "combine",
"options": {
"clashHandling": {
"values": {
"resolveClash": "preferInput1"
}
}
},
"joinMode": "enrichInput1",
"fieldsToMatchString": "id"
},
"typeVersion": 3.2
},
{
"id": "bda0d23c-f6fe-435c-8732-c1f97c313d80",
"name": "Configuration",
"type": "n8n-nodes-base.set",
"position": [
-2576,
864
],
"parameters": {
"values": {
"number": [
{
"name": "report.tg.chatID",
"value": null
}
],
"string": [
{
"name": "repo.owner"
},
{
"name": "repo.name"
},
{
"name": "repo.path",
"value": "workflows/"
}
],
"boolean": [
{
"name": "report.verbose"
}
]
},
"options": {}
},
"typeVersion": 1
},
{
"id": "a9074602-25a9-4fec-a464-caf1459dd3fb",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3024,
1072
],
"parameters": {
"width": 672,
"height": 960,
"content": "## Advanced n8n Workflow Sync with GitHub\n\nThis workflow automatically backs up your n8n workflows to a GitHub repository. It intelligently detects changes, handles workflow renames, and commits only when actual modifications occur, providing a clean version history.\n\n### \u2728 Key Features:\n- **Intelligent Sync**: Reliable backup of n8n workflows to GitHub.\n- **Rename Support**: Automatically handles workflow renames.\n- **Efficient Commits**: Only commits real changes, keeping your repo clean.\n- **Clear History**: Informative commit messages (create, update, rename).\n\n### \ud83d\ude80 Quick Setup:\n1. **Credentials**: Set up GitHub, n8n API, and optional Telegram credentials in n8n.\n2. **Configuration Node**: Open the `Configuration` node (green) and update:\n - `repo.owner`: Your GitHub username\n - `repo.name`: Your GitHub repository name\n - `repo.path`: Folder for backups (e.g., `workflows/`)\n - `report.tg.chatID` (Optional): Telegram chat ID, or `0` to disable.\n3. **Connect Credentials**: Link your created credentials to the respective GitHub, n8n, and Telegram nodes.\n4. **Schedule Trigger**: Adjust the backup frequency in the `Schedule Trigger` node.\n5. **Activate**: Save and activate the workflow.\n\n### \u2699\ufe0f How It Works (Simple Steps)\n\n1. **Get n8n Workflows**: The workflow starts by fetching all your current workflows from n8n.\n2. **Get GitHub Files**: At the same time, it lists all existing workflow files from your GitHub repository.\n3. **Compare & Decide**: It then compares each n8n workflow with its GitHub counterpart. It checks if anything changed, if it was renamed, or if it's new.\n4. **Take Action**:\n * If a workflow is **new**, it's created on GitHub.\n * If a workflow is **updated**, the file content is changed on GitHub.\n * If a workflow was **renamed**, the old file is deleted, and a new one is created.\n * If **nothing changed**, the workflow is skipped.\n5. **Send Report**: Finally, it can send a summary report to Telegram about what happened.\n\n### \ud83d\udca1 What's Next?\nFuture updates will include automatic archiving of inactive workflows and performance optimizations. Follow my profile for new workflow publications!"
},
"typeVersion": 1
},
{
"id": "999b6d5e-ba67-4b98-9637-1ca8c061113e",
"name": "Stop on empty config",
"type": "n8n-nodes-base.stopAndError",
"position": [
-1920,
1024
],
"parameters": {
"errorMessage": "Incomplete GitHub configuration. Please check \"Configuration\" node."
},
"typeVersion": 1
},
{
"id": "99820b4c-3ad8-43ba-bafe-9ae5f919e39f",
"name": "Assert GitHub config",
"type": "n8n-nodes-base.if",
"notes": "Pre-provisioning safe fuse",
"position": [
-2160,
864
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "0b285299-edd5-41a0-85e8-3d94246e1cff",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.repo.owner }}",
"rightValue": ""
},
{
"id": "c9f894e0-cf42-45e1-87bd-13c2bd024b48",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.repo.name }}",
"rightValue": ""
},
{
"id": "f1591996-df67-4caf-8171-a049993268d2",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json.repo.path }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.2
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "02eb2192-2fce-4283-90c0-616f2e6ced5d",
"connections": {
"Merge": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Router": {
"main": [
[
{
"node": "Delete old file",
"type": "main",
"index": 0
},
{
"node": "Merge after delete (rename)",
"type": "main",
"index": 0
}
],
[
{
"node": "Update file content and commit",
"type": "main",
"index": 0
},
{
"node": "Merge after update",
"type": "main",
"index": 0
}
],
[
{
"node": "Create new file",
"type": "main",
"index": 0
},
{
"node": "Merge after create",
"type": "main",
"index": 0
}
],
[
{
"node": "No Operation, do nothing",
"type": "main",
"index": 0
}
],
[
{
"node": "Stop and Error",
"type": "main",
"index": 0
}
]
]
},
"Get files": {
"main": [
[
{
"node": "Extract workflow parameters",
"type": "main",
"index": 0
}
]
]
},
"List files": {
"main": [
[
{
"node": "Get files",
"type": "main",
"index": 0
}
]
]
},
"Configuration": {
"main": [
[
{
"node": "Assert GitHub config",
"type": "main",
"index": 0
}
]
]
},
"Decide changes": {
"main": [
[
{
"node": "Router",
"type": "main",
"index": 0
}
]
]
},
"Render summary": {
"main": [
[
{
"node": "Send a message",
"type": "main",
"index": 0
}
]
]
},
"Create new file": {
"main": [
[
{
"node": "Merge after create",
"type": "main",
"index": 1
}
]
]
},
"Delete old file": {
"main": [
[
{
"node": "Merge after delete (rename)",
"type": "main",
"index": 1
}
]
]
},
"Loop Over Items": {
"main": [
[
{
"node": "Is Telegram configured?",
"type": "main",
"index": 0
}
],
[
{
"node": "Decide changes",
"type": "main",
"index": 0
}
]
]
},
"Schedule Trigger": {
"main": [
[
{
"node": "Configuration",
"type": "main",
"index": 0
}
]
]
},
"Anything changed?": {
"main": [
[
{
"node": "Render summary",
"type": "main",
"index": 0
}
]
]
},
"Get all workflows": {
"main": [
[
{
"node": "Encode N8N workflows",
"type": "main",
"index": 0
}
]
]
},
"Merge after create": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Merge after update": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Assert GitHub config": {
"main": [
[
{
"node": "Get all workflows",
"type": "main",
"index": 0
},
{
"node": "List files",
"type": "main",
"index": 0
}
],
[
{
"node": "Stop on empty config",
"type": "main",
"index": 0
}
]
]
},
"Build summary arrays": {
"main": [
[
{
"node": "Anything changed?",
"type": "main",
"index": 0
}
]
]
},
"Encode N8N workflows": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 0
}
]
]
},
"Is Telegram configured?": {
"main": [
[
{
"node": "Build summary arrays",
"type": "main",
"index": 0
}
]
]
},
"Create new file (rename)": {
"main": [
[
{
"node": "Merge after create (rename)",
"type": "main",
"index": 1
}
]
]
},
"No Operation, do nothing": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Extract workflow parameters": {
"main": [
[
{
"node": "Merge",
"type": "main",
"index": 1
}
]
]
},
"Merge after create (rename)": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Merge after delete (rename)": {
"main": [
[
{
"node": "Merge after create (rename)",
"type": "main",
"index": 0
},
{
"node": "Create new file (rename)",
"type": "main",
"index": 0
}
]
]
},
"Update file content and commit": {
"main": [
[
{
"node": "Merge after update",
"type": "main",
"index": 1
}
]
]
}
}
}
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.
githubApin8nApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
A robust workflow to back up and synchronize your n8n workflows to a GitHub repository, with intelligent change detection and support for file renames.
Source: https://n8n.io/workflows/7304/ — 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.
The native n8n security audit misses tokens pasted into node parameters, active workflows without an error handler, plain http:// calls and leftover pinned data. This workflow runs the native audit pl
Backup n8n workflows. Uses dateTime, github, itemLists, n8n. Scheduled trigger; 20 nodes.
Backup n8n workflows. Uses dateTime, github, itemLists, n8n. Scheduled trigger; 20 nodes.
Stop the panic attacks. We've all been there - accidentally deleted a workflow that took hours to build, or worse, corrupted your entire automation setup. This workflow is your safety net.
rss-telegram. Uses telegram, rssFeedRead, stopAndError. Scheduled trigger; 19 nodes.