Workflow n8n avancé synchronisé avec GitHub
Ceci est unDevOps, Multimodal AIworkflow d'automatisation du domainecontenant 38 nœuds.Utilise principalement des nœuds comme If, N8n, Set, Code, Merge. Sauvegarde automatisée des workflows avec une détection intelligente des changements via GitHub
- •Personal Access Token GitHub
- •Token Bot Telegram
Nœuds utilisés (38)
Catégorie
{
"id": "dmvTNU9rfNdgTeSp",
"meta": {
"instanceId": "e7cc7f71b8002726158f14502c7243f892bdf0befb8af4790197437e4666e71e"
},
"name": "Advanced n8n Workflow Sync with GitHub",
"tags": [],
"nodes": [
{
"id": "09450fb6-e815-4c7b-88e9-a92da4d70d16",
"name": "Boucle sur les éléments",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-368,
912
],
"parameters": {
"options": {}
},
"executeOnce": false,
"typeVersion": 3
},
{
"id": "34869a4d-b687-44f7-81fd-5f71e8679a0e",
"name": "Mettre à jour le contenu du fichier et valider",
"type": "n8n-nodes-base.github",
"position": [
1584,
1872
],
"webhookId": "f2d754dd-b68d-41e8-a662-7e91c1c3aa95",
"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": {
"id": "7sCS6E9S2UWO6PFt",
"name": "GitHub account"
}
},
"typeVersion": 1
},
{
"id": "bbf77bc7-8c35-4177-be4f-44c91682fd1a",
"name": "Déclencheur planifié",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-2912,
864
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "34d67ff9-e25a-42a9-b751-dbde37ed575b",
"name": "Obtenir tous les workflows",
"type": "n8n-nodes-base.n8n",
"position": [
-1328,
848
],
"parameters": {
"filters": {},
"requestOptions": {}
},
"credentials": {
"n8nApi": {
"id": "M9BEPZyx4jMbY5tY",
"name": "n8n account"
}
},
"typeVersion": 1
},
{
"id": "98fc6907-d188-4f9a-b68e-44592bfac95b",
"name": "Encoder les workflows N8N",
"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": "Décider des changements",
"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": "Supprimer l'ancien fichier",
"type": "n8n-nodes-base.github",
"position": [
1744,
1408
],
"webhookId": "1bd59af3-c8ee-4664-9cfd-df3ab4b6793d",
"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": {
"id": "7sCS6E9S2UWO6PFt",
"name": "GitHub account"
}
},
"typeVersion": 1
},
{
"id": "c6464638-d7d4-4e36-b359-f70412db2bbb",
"name": "Créer un nouveau fichier (renommer)",
"type": "n8n-nodes-base.github",
"position": [
2144,
1408
],
"webhookId": "1b7a1463-d11a-4c0c-a596-a4b09e003d5a",
"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": {
"id": "7sCS6E9S2UWO6PFt",
"name": "GitHub account"
}
},
"typeVersion": 1
},
{
"id": "b470dd47-762a-4c8a-90e0-e4f535e9b41e",
"name": "Fusionner après création (renommer)",
"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": "Fusionner après mise à jour",
"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": "Lister les fichiers",
"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
],
"webhookId": "2e1f9567-52d4-4047-980c-6b4a57d4bd40",
"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": {
"id": "7sCS6E9S2UWO6PFt",
"name": "GitHub account"
}
},
"typeVersion": 1.1
},
{
"id": "8c6f381b-caed-4267-abb4-6672c93f45e9",
"name": "Fusionner",
"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": "Note adhésive",
"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": "Note adhésive1",
"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": "Note adhésive2",
"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": "Note adhésive3",
"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": "Note adhésive4",
"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": "Construire les tableaux de résumé",
"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": "Générer le résumé",
"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": "Des changements ?",
"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": "Extraire les paramètres du workflow",
"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": "Obtenir les fichiers",
"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
],
"webhookId": "93c8a2dd-ddad-4837-a062-25473eee1208",
"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": {
"id": "7sCS6E9S2UWO6PFt",
"name": "GitHub account"
}
},
"typeVersion": 1.1
},
{
"id": "d991e89f-d795-4559-9b3f-4ecce50723b5",
"name": "Telegram est-il configuré ?",
"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": "Envoyer un message",
"type": "n8n-nodes-base.telegram",
"position": [
1152,
112
],
"webhookId": "ea0e343d-7ffa-4dd7-a3aa-0e7e45ad5753",
"parameters": {
"text": "={{ $json.message }}",
"chatId": "={{ $node[\"Configuration\"].json.report.tg.chatID }}",
"additionalFields": {
"parse_mode": "MarkdownV2",
"appendAttribution": false
}
},
"credentials": {
"telegramApi": {
"id": "7sLcbl1lRhPnfzJI",
"name": "Telegram account"
}
},
"typeVersion": 1.2
},
{
"id": "fdef22d8-7960-48e2-a046-e69b3f48ce15",
"name": "Créer un nouveau fichier",
"type": "n8n-nodes-base.github",
"position": [
1296,
2416
],
"webhookId": "66429ae4-4b7d-4fb5-8438-26cdf6c4faa8",
"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": {
"id": "7sCS6E9S2UWO6PFt",
"name": "GitHub account"
}
},
"typeVersion": 1
},
{
"id": "669101cb-2d23-41c7-8e07-6e282b556cdd",
"name": "Fusionner après création",
"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": "Note adhésive5",
"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": "Note adhésive6",
"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": "Aucune opération, ne rien faire",
"type": "n8n-nodes-base.noOp",
"position": [
1056,
2976
],
"parameters": {},
"typeVersion": 1
},
{
"id": "7e069ed2-7303-4db5-b0b3-f18edbfb88da",
"name": "Note adhésive7",
"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": "Arrêter et erreur",
"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": "Note adhésive8",
"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": "Routeur",
"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": "Fusionner après suppression (renommer)",
"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": "Note adhésive10",
"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### ✨ 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### 🚀 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### ⚙️ 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### 💡 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": "Arrêt si configuration vide",
"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": "Vérifier la configuration GitHub",
"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,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "02eb2192-2fce-4283-90c0-616f2e6ced5d",
"connections": {
"8c6f381b-caed-4267-abb4-6672c93f45e9": {
"main": [
[
{
"node": "09450fb6-e815-4c7b-88e9-a92da4d70d16",
"type": "main",
"index": 0
}
]
]
},
"4d426ef2-c4db-4690-bd23-48c374fcf0e1": {
"main": [
[
{
"node": "1cc5a35b-8e52-46dd-ac69-03e320f66fc8",
"type": "main",
"index": 0
},
{
"node": "921826cc-13da-4db8-8a18-fdffacbcf5bb",
"type": "main",
"index": 0
}
],
[
{
"node": "34869a4d-b687-44f7-81fd-5f71e8679a0e",
"type": "main",
"index": 0
},
{
"node": "be267762-77a4-4af5-ba26-7f3945d44b32",
"type": "main",
"index": 0
}
],
[
{
"node": "fdef22d8-7960-48e2-a046-e69b3f48ce15",
"type": "main",
"index": 0
},
{
"node": "669101cb-2d23-41c7-8e07-6e282b556cdd",
"type": "main",
"index": 0
}
],
[
{
"node": "2ababf2c-5315-4d6a-9f09-b2efaef4ab2c",
"type": "main",
"index": 0
}
],
[
{
"node": "0ac9743d-298a-49d2-b391-4c34b9d5122b",
"type": "main",
"index": 0
}
]
]
},
"3464840a-599d-4432-aade-bfd5a2930461": {
"main": [
[
{
"node": "13ba1e58-2bac-44dc-9142-b61efef7c4e1",
"type": "main",
"index": 0
}
]
]
},
"db1f95ae-42e5-4588-bb7d-5348011d9afe": {
"main": [
[
{
"node": "3464840a-599d-4432-aade-bfd5a2930461",
"type": "main",
"index": 0
}
]
]
},
"bda0d23c-f6fe-435c-8732-c1f97c313d80": {
"main": [
[
{
"node": "99820b4c-3ad8-43ba-bafe-9ae5f919e39f",
"type": "main",
"index": 0
}
]
]
},
"3576b4b3-6692-4d08-9453-697d44ac0f56": {
"main": [
[
{
"node": "4d426ef2-c4db-4690-bd23-48c374fcf0e1",
"type": "main",
"index": 0
}
]
]
},
"0c401771-b522-4091-a14f-aba4a06d6158": {
"main": [
[
{
"node": "0705a90f-00e1-46e6-b5ef-4a4efe5255cd",
"type": "main",
"index": 0
}
]
]
},
"fdef22d8-7960-48e2-a046-e69b3f48ce15": {
"main": [
[
{
"node": "669101cb-2d23-41c7-8e07-6e282b556cdd",
"type": "main",
"index": 1
}
]
]
},
"1cc5a35b-8e52-46dd-ac69-03e320f66fc8": {
"main": [
[
{
"node": "921826cc-13da-4db8-8a18-fdffacbcf5bb",
"type": "main",
"index": 1
}
]
]
},
"09450fb6-e815-4c7b-88e9-a92da4d70d16": {
"main": [
[
{
"node": "d991e89f-d795-4559-9b3f-4ecce50723b5",
"type": "main",
"index": 0
}
],
[
{
"node": "3576b4b3-6692-4d08-9453-697d44ac0f56",
"type": "main",
"index": 0
}
]
]
},
"bbf77bc7-8c35-4177-be4f-44c91682fd1a": {
"main": [
[
{
"node": "bda0d23c-f6fe-435c-8732-c1f97c313d80",
"type": "main",
"index": 0
}
]
]
},
"fdb5e4f7-8aa1-4273-8776-ac0248b893e4": {
"main": [
[
{
"node": "0c401771-b522-4091-a14f-aba4a06d6158",
"type": "main",
"index": 0
}
]
]
},
"34d67ff9-e25a-42a9-b751-dbde37ed575b": {
"main": [
[
{
"node": "98fc6907-d188-4f9a-b68e-44592bfac95b",
"type": "main",
"index": 0
}
]
]
},
"669101cb-2d23-41c7-8e07-6e282b556cdd": {
"main": [
[
{
"node": "09450fb6-e815-4c7b-88e9-a92da4d70d16",
"type": "main",
"index": 0
}
]
]
},
"be267762-77a4-4af5-ba26-7f3945d44b32": {
"main": [
[
{
"node": "09450fb6-e815-4c7b-88e9-a92da4d70d16",
"type": "main",
"index": 0
}
]
]
},
"99820b4c-3ad8-43ba-bafe-9ae5f919e39f": {
"main": [
[
{
"node": "34d67ff9-e25a-42a9-b751-dbde37ed575b",
"type": "main",
"index": 0
},
{
"node": "db1f95ae-42e5-4588-bb7d-5348011d9afe",
"type": "main",
"index": 0
}
],
[
{
"node": "999b6d5e-ba67-4b98-9637-1ca8c061113e",
"type": "main",
"index": 0
}
]
]
},
"d0af7c02-a586-4993-8c4b-8fb18c3053fe": {
"main": [
[
{
"node": "fdb5e4f7-8aa1-4273-8776-ac0248b893e4",
"type": "main",
"index": 0
}
]
]
},
"98fc6907-d188-4f9a-b68e-44592bfac95b": {
"main": [
[
{
"node": "8c6f381b-caed-4267-abb4-6672c93f45e9",
"type": "main",
"index": 0
}
]
]
},
"d991e89f-d795-4559-9b3f-4ecce50723b5": {
"main": [
[
{
"node": "d0af7c02-a586-4993-8c4b-8fb18c3053fe",
"type": "main",
"index": 0
}
]
]
},
"c6464638-d7d4-4e36-b359-f70412db2bbb": {
"main": [
[
{
"node": "b470dd47-762a-4c8a-90e0-e4f535e9b41e",
"type": "main",
"index": 1
}
]
]
},
"2ababf2c-5315-4d6a-9f09-b2efaef4ab2c": {
"main": [
[
{
"node": "09450fb6-e815-4c7b-88e9-a92da4d70d16",
"type": "main",
"index": 0
}
]
]
},
"13ba1e58-2bac-44dc-9142-b61efef7c4e1": {
"main": [
[
{
"node": "8c6f381b-caed-4267-abb4-6672c93f45e9",
"type": "main",
"index": 1
}
]
]
},
"b470dd47-762a-4c8a-90e0-e4f535e9b41e": {
"main": [
[
{
"node": "09450fb6-e815-4c7b-88e9-a92da4d70d16",
"type": "main",
"index": 0
}
]
]
},
"921826cc-13da-4db8-8a18-fdffacbcf5bb": {
"main": [
[
{
"node": "b470dd47-762a-4c8a-90e0-e4f535e9b41e",
"type": "main",
"index": 0
},
{
"node": "c6464638-d7d4-4e36-b359-f70412db2bbb",
"type": "main",
"index": 0
}
]
]
},
"34869a4d-b687-44f7-81fd-5f71e8679a0e": {
"main": [
[
{
"node": "be267762-77a4-4af5-ba26-7f3945d44b32",
"type": "main",
"index": 1
}
]
]
}
}
}Comment utiliser ce workflow ?
Copiez le code de configuration JSON ci-dessus, créez un nouveau workflow dans votre instance n8n et sélectionnez "Importer depuis le JSON", collez la configuration et modifiez les paramètres d'authentification selon vos besoins.
Dans quelles scénarios ce workflow est-il adapté ?
Avancé - DevOps, IA Multimodale
Est-ce payant ?
Ce workflow est entièrement gratuit et peut être utilisé directement. Veuillez noter que les services tiers utilisés dans le workflow (comme l'API OpenAI) peuvent nécessiter un paiement de votre part.
Workflows recommandés
Maksym Brashenko
@j2h4uI’m a generalist engineer who thrives on messy systems, undocumented protocols, and problems no one wants to touch Over the past 6 years, I’ve built infrastructure for managing hardware, reverse-engineered flaky devices, automated workflows end to end, and connected product logic to business reality Use my link to book an initial consultation
Partager ce workflow