This workflow corresponds to n8n.io template #17352 — we link there as the canonical source.
This workflow follows the GitHub → HTTP Request recipe pattern — see all workflows that pair these two integrations.
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": "n5Yo7gghuWYovbIi",
"meta": {
"builderVariant": "mcp",
"aiBuilderAssisted": true
},
"name": "GitOps Engine",
"tags": [],
"nodes": [
{
"id": "6177870d-b746-4288-9bfe-9e4536e1b2dc",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-1328,
448
],
"parameters": {},
"typeVersion": 1
},
{
"id": "87b324b7-06da-4a40-84f7-321659b7e5b7",
"name": "config",
"type": "n8n-nodes-base.set",
"position": [
-1056,
448
],
"parameters": {
"mode": "raw",
"options": {},
"jsonOutput": "={{\n {\n //Nombre del repo donde se guadar\u00e1n/extraer\u00e1n los respaldos\n repo_name: 'n8n_coolify_workflows',\n\n //Nombre del usuario propietario del repo\n repo_owner: 'junforever',\n\n //Tipo de repo para la creaci\u00f3n si no existe, true para repo privado, false para p\u00fablico\n repo_private: true,\n\n //Descripci\u00f3n del repo para la creaci\u00f3n si no existe\n repo_description: 'Repositorio de respaldo de todos los flujos de n8n',\n\n //Versi\u00f3n del API de Github\n api_version: '2022-11-28',\n\n //Mensaje para el commit de un nuevo flujo\n commit_added: 'Nuevo workflow',\n\n //Mensaje para el commit de un flujo que se va a eliminar para ser reemplazado por la versi\u00f3n con el nuevo nombre\n commit_deleted: 'Workflow <name> renombrado a <new_name>',\n\n //Mensaje para el commit de un flujo eliminado\n commit_clean: 'Workflow elimininado',\n\n //Mensaje para el commit de un flujo que fue renombrado\n commit_renamed: 'Workflow <name> renombrado',\n\n //Mensaje para el commit de un flujo que fur actualizado\n commit_updated: 'Workflow actualizado',\n\n //Caracter para la separaci\u00f3n de los bloques del nombre de los archivos, NO USAR - ni _ porque algunos ids pueden incluir dichos caracteres\n separator_character: ':'\n }\n}}\n"
},
"typeVersion": 3.4
},
{
"id": "6e184763-3e3e-4de1-a37a-bfc36ec35807",
"name": "create_repo",
"type": "n8n-nodes-base.httpRequest",
"position": [
1072,
656
],
"parameters": {
"url": "https://api.github.com/user/repos",
"method": "POST",
"options": {},
"jsonBody": "={{\n {\n name: $('config').item.json.repo_name,\n description: $('config').item.json.repo_description,\n private: $('config').item.json.repo_private\n }\n}}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"headerParameters": {
"parameters": [
{
"name": "X-GitHub-Api-Version",
"value": "={{ $('config').first().json.api_version }}"
}
]
},
"nodeCredentialType": "githubOAuth2Api"
},
"credentials": {
"githubApi": {
"name": "<your credential>"
},
"githubOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.3
},
{
"id": "612f0e64-f118-459c-9115-3e0b46d8ebf8",
"name": "get_workflows",
"type": "n8n-nodes-base.n8n",
"position": [
-752,
448
],
"parameters": {
"filters": {
"excludePinnedData": true
},
"requestOptions": {}
},
"credentials": {
"n8nApi": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "e17f74b5-f7c7-4555-b990-d071c8a8eb83",
"name": "get_not_archived",
"type": "n8n-nodes-base.filter",
"position": [
-512,
448
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "7663efd7-8875-420a-abc9-8cc16fc1d2df",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.isArchived }}",
"rightValue": false
}
]
}
},
"typeVersion": 2.3,
"alwaysOutputData": true
},
{
"id": "a0c2a68e-395e-4436-9f36-26a3e85cac71",
"name": "list_files",
"type": "n8n-nodes-base.github",
"onError": "continueErrorOutput",
"position": [
1056,
336
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').item.json.repo_owner }}"
},
"resource": "file",
"operation": "list",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').item.json.repo_name }}"
},
"authentication": "oAuth2"
},
"credentials": {
"githubOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1.1,
"alwaysOutputData": true
},
{
"id": "5aea626a-1e7f-4f3c-a3e1-151d92ac83a4",
"name": "exists_workflows",
"type": "n8n-nodes-base.if",
"position": [
-272,
448
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "682bde8c-5869-4a1c-8a36-d1a82c148885",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $('get_not_archived').all().length }}",
"rightValue": 0
}
]
}
},
"executeOnce": true,
"typeVersion": 2.3
},
{
"id": "2b32c55d-17bc-4f96-b715-2fc2e7e0ca5a",
"name": "do_nothing",
"type": "n8n-nodes-base.noOp",
"position": [
-272,
704
],
"parameters": {},
"typeVersion": 1
},
{
"id": "f22d83fe-a21d-44af-b3d0-8a07a4de598e",
"name": "get_file_sha",
"type": "n8n-nodes-base.code",
"position": [
64,
432
],
"parameters": {
"jsCode": "const crypto = require('crypto');\n\n/**\n * Sanitiza el nombre del workflow para eliminar caracteres conflictivos antes de subir el archivo a github\n */\nfunction sanitizeWorkflowName(name) {\n const replaceBy = '_'\n return (\n String(name ?? '')\n .replaceAll(/[-<>:\"/\\\\|?*\\u0000-\\u001F\\u007F]/g, replaceBy)\n .trim() ||\n 'workflow'\n );\n}\n\n/**\n * Ordena recursivamente las claves de los objetos\n * IMPORTANTE: NO ordena arrays aqu\u00ed solo ordena las keys de sus elementos que sean tipo object\n */\nfunction sortObject(value) {\n if (Array.isArray(value)) {\n return value.map(sortObject);\n }\n if (value && typeof value === 'object') {\n return Object.keys(value)\n .sort()\n .reduce((acc, key) => {\n acc[key] = sortObject(value[key]);\n return acc;\n }, {});\n }\n return value;\n}\n\n/**\n * Normaliza y ordena determin\u00edsticamente las conexiones\n */\nfunction normalizeConnections(connections = {}) {\n const sorted = sortObject(connections);\n\n for (const sourceNode of Object.keys(sorted)) {\n for (const outputType of Object.keys(sorted[sourceNode])) {\n sorted[sourceNode][outputType] = sorted[sourceNode][outputType].map(\n (connectionArray) =>\n connectionArray\n .map(sortObject)\n .sort((a, b) =>\n `${a.node}:${a.index}`.localeCompare(`${b.node}:${b.index}`)\n )\n );\n }\n }\n\n return sorted;\n}\n\nfunction canonicalizeWorkflow(workflow) {\n /**\n * Canonicalizaci\u00f3n de nodos\n * - Excluye node.id\n * - Excluye credentials.id\n * - Mantiene name y position\n * - ORDENA los nodos por name (clave estable)\n */\n const canonicalNodes = workflow.nodes\n .map((node) => {\n const { id, credentials, ...rest } = node;\n\n const canonicalNode = sortObject(rest);\n\n if (credentials) {\n canonicalNode.credentials = {};\n for (const [credType, credValue] of Object.entries(credentials)) {\n canonicalNode.credentials[credType] = {\n name: credValue.name,\n };\n }\n }\n\n return canonicalNode;\n })\n .sort((a, b) => a.name.localeCompare(b.name));\n\n /**\n * Workflow can\u00f3nico final\n * SOLO toma los campos que definen el comportamiento real\n */\n const canonicalWorkflow = {\n nodes: canonicalNodes,\n connections: normalizeConnections(workflow.connections),\n settings: sortObject(workflow.settings ?? {}),\n };\n\n return sortObject(canonicalWorkflow);\n}\n\n/* =========================\n Generaci\u00f3n de SHA\n ========================= */\n\nlet response = [];\n\nfor (const item of $('get_not_archived').all()) {\n const canonical = canonicalizeWorkflow(item.json);\n const canonicalString = JSON.stringify(canonical);\n\n const sha = crypto\n .createHash('sha256')\n .update(canonicalString)\n .digest('hex');\n\n response.push({\n sha,\n id: item.json.id,\n name: sanitizeWorkflowName(item.json.name),\n content: item.json,\n });\n}\n\nreturn response;\n"
},
"typeVersion": 2
},
{
"id": "eaa50ca3-7947-4868-95e9-c14f9e91d8ad",
"name": "workflows_init",
"type": "n8n-nodes-base.code",
"position": [
1392,
656
],
"parameters": {
"jsCode": "return $('get_file_sha').all();"
},
"typeVersion": 2
},
{
"id": "8412dacf-a3ae-476f-b7fa-f6305a275e56",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-816,
-48
],
"parameters": {
"color": 4,
"width": 736,
"height": 956,
"content": "## Step 1\n\n- Retrieve all workflows available in n8n.\n- Keep only workflows that are not archived.\n- Verify that at least one workflow remains to be backed up.\n\n### Scope and operation\n\n- By default, archived workflows are not backed up. Disable or delete `get_not_archived` if you need to back up every workflow.\n- The Manual Trigger is included for testing. For production, replace it with a Schedule/Cron Trigger and activate the workflow so production executions and error-workflow notifications can run.\n- Avoid simultaneous executions targeting the same repository. Use one backup workflow per repository and choose a schedule that allows each execution to finish before the next one starts.\n- The workflow timeout is currently 300 seconds. Increase it in Workflow Settings when backing up many workflows or when the network connection is slow.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
},
"typeVersion": 1
},
{
"id": "0a693abb-1f1e-4a24-8501-6a4e469a667f",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-32,
-976
],
"parameters": {
"color": 4,
"width": 368,
"height": 1892,
"content": "## Step 2\n\nGenerate a deterministic semantic SHA for every workflow.\n\n### SHA criteria\n\nOnly `nodes`, `connections`, and `settings` are included in the canonical representation.\n\n#### Excluded at workflow level\n\n- `updatedAt`\n- `createdAt`\n- `versionId`\n- `activeVersionId`\n- `activeVersion`\n- `triggerCount`\n- `staticData`\n- `meta`\n- `shared`\n- `tags`\n- `name`\n- `active`\n- `isArchived` (already filtered)\n- `id` (used only in the filename)\n\n#### Excluded at node and credential level\n\n- Internal node UUID: `node.id`\n- Credential IDs: `credentials.*.id`\n\n### Important notes\n\n- The `shared` section is intentionally excluded because workflow sharing is a paid feature and does not apply to Community Edition.\n- This is a semantic SHA, not the GitHub blob SHA.\n- Existing workflow JSON files are not downloaded from GitHub. Therefore, manual changes made directly to their contents are not detected while the semantic SHA stored in the filename remains unchanged.\n- Workflow names are sanitized before building filenames. The current sanitizer replaces `-`, `<`, `>`, `:`, `\"`, `/`, `\\`, `|`, `?`, `*`, ASCII control characters `U+0000\u2013U+001F`, and `U+007F` with `_`. This is only for the file name in Github, the workflow name inside the json file doesn't change.\n- If you change `separator_character`, make sure the new separator is also replaced by `sanitizeWorkflowName` so it cannot break the filename structure.\n\n\n\n\n\n\n\n\n\n\n"
},
"typeVersion": 1
},
{
"id": "ec749936-2554-47e5-bdd8-ad104139f118",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
400,
-496
],
"parameters": {
"color": 4,
"width": 1648,
"height": 2104,
"content": "## Step 3\n\n- Verify whether the configured GitHub repository exists.\n- Route the response by category:\n - Success: list the files in the repository.\n - `404`: follow the repository-creation path.\n - Authentication, permissions, API rate limit, connectivity, and unexpected failures: stop the execution with the corresponding Stop And Error node.\n- If the repository does not exist, create it using `repo_private` and then create one backup file for every item from `get_file_sha`.\n- List the files stored in the repository root and compare them with the current n8n workflows in `files_analizer`.\n\n### Classification\n\n1. Workflow content changed but its name did not.\n2. Workflow was renamed, regardless of whether its content also changed.\n3. Workflow is new.\n4. Workflow was deleted from n8n.\n\nIf the repository is empty, create all backup files from `get_file_sha`.\n\n### Repository and filename behavior\n\n- Backup files use the format `<workflow_id><separator><semantic_sha><separator><workflow_name>.json`.\n- Only files matching that structure are considered workflow backups; files such as `README.md` or unrelated JSON files are ignored.\n- All backups are stored in the repository root. This template does not create subfolders or reproduce the n8n folder hierarchy.\n- By default, a missing repository is created as private. Set `repo_private` to `false` if a public repository is required.\n- GitHub may return `404` both when the repository does not exist and when it is private but the credential cannot access it. This workflow treats both responses as a missing repository and follows the creation path.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
},
"typeVersion": 1
},
{
"id": "175dc6b2-33ca-4e95-b68e-c3d124aee383",
"name": "files_analizer",
"type": "n8n-nodes-base.code",
"position": [
1712,
304
],
"parameters": {
"jsCode": "/*** \n* Entradas \n***/\n\n//workflows de n8n\nconst n8nWorkflows = $('get_file_sha').all().map(node => node.json); \n\n//archivos del repo de Github\nconst githubFiles = $('list_files').all().map(node => node.json); \n\n//caracter separador\nconst separatorCharacter = $('config').first().json.separator_character;\n\n/*** \n* Helpers \n***/\n\n//Extraer los bloques de informaci\u00f3n del nombre de los archivos de github <id>:<sha>:<name>.json\nfunction parseGithubFilename(filename) {\n const clean = filename.replace(/\\.json$/, '');\n const parts = clean.split(separatorCharacter);\n\n return {\n id: parts[0],\n sha: parts[1],\n name: parts.slice(2).join(separatorCharacter)\n };\n}\n\n//Ignorar archivos de Github que no sigan el patr\u00f3n de nombre de los flujos (README, otros json, etc.)\nfunction isWorkflowFile(filename, escapedSeparator = separatorCharacter) {\n const regex = new RegExp(\n `^[^${escapedSeparator}]+${escapedSeparator}[a-f0-9]{8,}${escapedSeparator}.*\\\\.json$`,\n 'i'\n );\n \n //return /^[^:]+:[a-f0-9]{8,}:.*\\.json$/i.test(filename);\n return regex.test(filename);\n}\n\n/*** \n* Pre procesamiento \n***/\n\n//Indexar los archivos de GitHub por id del workflow\nconst githubById = new Map();\n\nfor (const file of githubFiles) {\n if (!isWorkflowFile(file.name)) {\n continue;\n }\n const parsed = parseGithubFilename(file.name);\n\n githubById.set(parsed.id, {\n ...parsed,\n fileSha: file.sha,\n path: file.path,\n originalName: file.name\n });\n}\n\n//Indexar los archivos de n8n por id del workflow\nconst n8nById = new Map();\nfor (const wf of n8nWorkflows) {\n n8nById.set(wf.id, wf);\n}\n\n/*** \n* L\u00f3gica de comparaci\u00f3n \n***/\n\n//Archivos que tienen cambios pero su nombre es el mismo\nconst changedContentSameName = [];\n\n//Archivos que fueron renombrados independientemente de si tuvieron cambios o no\nconst renamed = [];\n\n//Nuevos workflows\nconst newWorkflows = [];\n\n//Workflows que ha sido eliminados de n8n\nconst deletedWorkflows = [];\n\n// Comparar n8n \u2192 GitHub\nfor (const wf of n8nWorkflows) {\n const gh = githubById.get(wf.id);\n\n // Nuevo workflow\n if (!gh) {\n newWorkflows.push(wf);\n continue;\n }\n\n const sameName = gh.name === wf.name;\n const sameSha = gh.sha === wf.sha;\n\n //Workflow Renombrado\n if (!sameName) {\n renamed.push({\n workflow: wf,\n github: gh\n });\n continue;\n }\n\n //Workflow con cambios pero el mismo nombre\n if (!sameSha) {\n changedContentSameName.push({\n workflow: wf,\n github: gh\n });\n }\n}\n\n//Workflow eliminado en n8n (GitHub \u2192 n8n)\nfor (const [id, gh] of githubById.entries()) {\n if (!n8nById.has(id)) {\n deletedWorkflows.push(gh);\n }\n}\n\n// item \u00fanico con los 4 arreglos\nreturn [\n {\n changedContentSameName,\n renamed,\n newWorkflows,\n deletedWorkflows\n }\n];\n"
},
"typeVersion": 2
},
{
"id": "afcb433b-12e0-443c-9e72-688989bf1121",
"name": "exists_files",
"type": "n8n-nodes-base.if",
"position": [
1408,
320
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "b10292a6-b300-4c10-8f15-2fd53332774e",
"operator": {
"type": "object",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $json }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2.3
},
{
"id": "b093ea13-08f6-4b10-9266-c3942684f409",
"name": "changed_content_same_name",
"type": "n8n-nodes-base.splitOut",
"position": [
2448,
-304
],
"parameters": {
"options": {},
"fieldToSplitOut": "changedContentSameName"
},
"typeVersion": 1
},
{
"id": "8142b236-1e77-4a8d-b546-49d8c1dca59c",
"name": "renamed",
"type": "n8n-nodes-base.splitOut",
"position": [
2432,
192
],
"parameters": {
"options": {},
"fieldToSplitOut": "renamed"
},
"typeVersion": 1
},
{
"id": "3de283e5-4af4-415a-95d7-13605b6501cb",
"name": "create_files",
"type": "n8n-nodes-base.github",
"position": [
1872,
816
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').item.json.repo_owner }}"
},
"filePath": "={{ $json.id }}{{ $('config').first().json.separator_character }}{{ $json.sha }}{{ $('config').first().json.separator_character }}{{ $json.name }}.json",
"resource": "file",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').item.json.repo_name }}"
},
"fileContent": "={{ JSON.stringify($json.content, null, 2) }}",
"commitMessage": "={{ $('config').item.json.commit_added }}",
"authentication": "oAuth2"
},
"credentials": {
"githubOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "65c6e93e-7efd-4bef-9220-03acee203704",
"name": "create_updated_file",
"type": "n8n-nodes-base.github",
"position": [
3024,
-160
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').first().json.repo_owner }}"
},
"filePath": "={{ $json.workflow.id }}{{ $('config').first().json.separator_character }}{{ $json.workflow.sha }}{{ $('config').first().json.separator_character }}{{ $json.workflow.name }}.json",
"resource": "file",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').first().json.repo_name }}"
},
"fileContent": "={{ JSON.stringify($json.workflow.content, null, 2) }}",
"commitMessage": "={{ $('config').first().json.commit_updated }}",
"authentication": "oAuth2"
},
"credentials": {
"githubOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "9a5c4af6-546c-4944-b93d-c6916f0205f8",
"name": "delete_previous_files",
"type": "n8n-nodes-base.github",
"position": [
3232,
368
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').first().json.repo_owner }}"
},
"filePath": "={{ $('loop_over_renamed').item.json.github.path }}",
"resource": "file",
"operation": "delete",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').first().json.repo_name }}"
},
"commitMessage": "={{ $('config').first().json.commit_deleted.replace('<name>',$('loop_over_renamed').item.json.github.name).replace('<new_name>', $('loop_over_renamed').item.json.workflow.name) }}",
"authentication": "oAuth2"
},
"credentials": {
"githubOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "f52ca85c-5413-4f44-9eeb-b0fb80d38036",
"name": "create_renamed_files",
"type": "n8n-nodes-base.github",
"position": [
2992,
368
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').first().json.repo_owner }}"
},
"filePath": "={{ $('renamed').item.json.workflow.id }}{{ $('config').first().json.separator_character }}{{ $('renamed').item.json.workflow.sha }}{{ $('config').first().json.separator_character }}{{ $('renamed').item.json.workflow.name }}.json",
"resource": "file",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').first().json.repo_name }}"
},
"fileContent": "={{ JSON.stringify($('renamed').item.json.workflow.content, null, 2) }}",
"commitMessage": "={{ $('config').first().json.commit_renamed.replace('<name>', $('renamed').item.json.github.name) }}",
"authentication": "oAuth2"
},
"credentials": {
"githubOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "e3d58e07-6a05-4a48-a906-156fd0b33871",
"name": "new_workflows",
"type": "n8n-nodes-base.splitOut",
"position": [
2416,
704
],
"parameters": {
"options": {},
"fieldToSplitOut": "newWorkflows"
},
"typeVersion": 1
},
{
"id": "d262d836-8ad7-44bc-85c2-0764addf8a41",
"name": "create_new_files",
"type": "n8n-nodes-base.github",
"position": [
3024,
848
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').first().json.repo_owner }}"
},
"filePath": "={{ $('new_workflows').item.json.id }}{{ $('config').first().json.separator_character }}{{ $('new_workflows').item.json.sha }}{{ $('config').first().json.separator_character }}{{ $('new_workflows').item.json.name }}.json",
"resource": "file",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').first().json.repo_name }}"
},
"fileContent": "={{ JSON.stringify($('new_workflows').item.json.content, null, 2) }}",
"commitMessage": "={{ $('config').first().json.commit_added }}",
"authentication": "oAuth2"
},
"credentials": {
"githubOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1
},
{
"id": "944e3257-1dec-497b-add1-89b50ab40f79",
"name": "deleted_workflows",
"type": "n8n-nodes-base.splitOut",
"position": [
2416,
1200
],
"parameters": {
"options": {},
"fieldToSplitOut": "deletedWorkflows"
},
"typeVersion": 1
},
{
"id": "002cf69d-476b-40b3-86cf-edfc2f21ecbd",
"name": "delete_files",
"type": "n8n-nodes-base.github",
"position": [
3040,
1360
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').first().json.repo_owner }}"
},
"filePath": "={{ $json.path }}",
"resource": "file",
"operation": "delete",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').first().json.repo_name }}"
},
"commitMessage": "={{ $('config').first().json.commit_clean }}",
"authentication": "oAuth2"
},
"credentials": {
"githubOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "e9439911-1f9e-4d8e-a5e7-4fb168a8cc87",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
2240,
-640
],
"parameters": {
"color": 4,
"width": 1280,
"height": 2284,
"content": "## Step 4\n\n- For a workflow whose content changed but whose name did not, first create the replacement file using the new semantic SHA and then delete the outdated file.\n- For a renamed workflow, first create the file with the new name and then delete the previous file, regardless of whether its content also changed.\n- Create backup files for new workflows.\n- Delete backup files for workflows that no longer exist in n8n.\n\nCreating the replacement before deleting the previous file reduces the risk of losing the last available backup if GitHub rejects the create operation.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
},
"typeVersion": 1
},
{
"id": "3ede4919-4bd4-4dac-9298-5bbfa404ab7e",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1152,
-640
],
"parameters": {
"color": 4,
"width": 304,
"height": 1548,
"content": "## Master Configuration\n\nSet the base values used throughout the workflow:\n\n- `repo_name`: GitHub repository used to store the backups.\n- `repo_owner`: GitHub user or organization that owns the repository.\n- `repo_private`: visibility used only when the repository must be created. The default is `true` (private); set it to `false` to create a public repository.\n- `repo_description`: description used when creating the repository.\n- `api_version`: GitHub REST API version.\n- Commit-message fields: messages used for create, update, rename, and delete operations.\n- `separator_character`: character used to separate workflow ID, semantic SHA, and workflow name in each backup filename.\n\n### Security and ownership\n\n- Store tokens and other sensitive values only in native n8n credentials. Never hardcode secrets in nodes, expressions, or Code nodes.\n- All GitHub and HTTP Request nodes must use the same GitHub OAuth2 credential.\n- The OAuth2 credential should belong to the user configured as `repo_owner`. For organization-owned repositories, that user must have the required repository permissions.\n- Publish this template with MCP access disabled. Anyone who enables MCP after importing it is responsible for that decision.\n\n\n\n\n\n\n\n\n\n\n"
},
"typeVersion": 1
},
{
"id": "f0bfbad7-5a80-451f-af00-d871a368b466",
"name": "Sticky Note5",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2048,
0
],
"parameters": {
"color": 5,
"width": 192,
"height": 112,
"content": "# GitOps Engine\n"
},
"typeVersion": 1
},
{
"id": "7ca3ed44-417f-4414-b85c-c4c9fb17b708",
"name": "files_created",
"type": "n8n-nodes-base.noOp",
"position": [
1872,
640
],
"parameters": {},
"typeVersion": 1
},
{
"id": "5d610f35-3908-4e4e-868a-d0335fe962b6",
"name": "loop_new_items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
1632,
656
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "bfae9e31-f53e-4ce6-a624-4c49f2029731",
"name": "files_updated",
"type": "n8n-nodes-base.noOp",
"position": [
3024,
-320
],
"parameters": {},
"typeVersion": 1
},
{
"id": "4e68cf4c-649c-4eb1-b74d-036d078dc0c5",
"name": "loop_over_renamed",
"type": "n8n-nodes-base.splitInBatches",
"position": [
2704,
192
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "161ed7b5-5c59-473b-920c-1cb7ec6536b7",
"name": "loop_over_updated",
"type": "n8n-nodes-base.splitInBatches",
"position": [
2704,
-304
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "fb21cb5f-de0d-4988-ab98-120c629fb5bc",
"name": "files_renamed",
"type": "n8n-nodes-base.noOp",
"position": [
2992,
176
],
"parameters": {},
"typeVersion": 1
},
{
"id": "f4be7efa-cf0a-4646-9a98-dfe37f0e5cce",
"name": "loop_over_created",
"type": "n8n-nodes-base.splitInBatches",
"position": [
2704,
704
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "c1d0347f-cf45-4a1f-b4ed-321d864f9c95",
"name": "files_created1",
"type": "n8n-nodes-base.noOp",
"position": [
3024,
688
],
"parameters": {},
"typeVersion": 1
},
{
"id": "81ccbd4b-4679-4d48-b714-e81365c08014",
"name": "loop_over_deleted",
"type": "n8n-nodes-base.splitInBatches",
"position": [
2720,
1200
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "b62fc2c3-fa7b-4d2a-b3a3-d19765e94cf1",
"name": "files_deleted",
"type": "n8n-nodes-base.noOp",
"position": [
3040,
1184
],
"parameters": {},
"typeVersion": 1
},
{
"id": "8f44a1ab-b984-473c-a254-4d941b9ac362",
"name": "delete_outdated_file",
"type": "n8n-nodes-base.github",
"position": [
3264,
-160
],
"parameters": {
"owner": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').first().json.repo_owner }}"
},
"filePath": "={{ $('changed_content_same_name').item.json.github.path }}",
"resource": "file",
"operation": "delete",
"repository": {
"__rl": true,
"mode": "name",
"value": "={{ $('config').first().json.repo_name }}"
},
"commitMessage": "={{ $('config').first().json.commit_updated }}",
"authentication": "oAuth2"
},
"credentials": {
"githubOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "7d054d12-5c2a-4be4-b8ac-e5893ca87504",
"name": "verify_repo",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueErrorOutput",
"position": [
464,
432
],
"parameters": {
"url": "={{ 'https://api.github.com/repos/' + $('config').item.json.repo_owner + '/' + $('config').item.json.repo_name }}",
"options": {
"timeout": 10000,
"response": {
"response": {
"neverError": true,
"fullResponse": true,
"responseFormat": "json"
}
}
},
"sendHeaders": true,
"authentication": "predefinedCredentialType",
"headerParameters": {
"parameters": [
{
"name": "Accept",
"value": "application/vnd.github+json"
},
{
"name": "X-GitHub-Api-Version",
"value": "={{ $('config').first().json.api_version }}"
}
]
},
"nodeCredentialType": "githubOAuth2Api"
},
"credentials": {
"githubOAuth2Api": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 4.4
},
{
"id": "fd134c23-f212-4b97-aaeb-6684e437d6b5",
"name": "github_authentication_error",
"type": "n8n-nodes-base.stopAndError",
"position": [
768,
752
],
"parameters": {
"errorMessage": "={{ 'Autenticaci\u00f3n con GitHub fallida (HTTP ' + ($json.statusCode ?? 'desconocido') + '). Revisa o renueva la credencial GitHub OAuth2. Detalle: ' + ($json.body?.message ?? $json.message ?? 'sin detalle') }}"
},
"typeVersion": 1
},
{
"id": "572617d2-0135-43fa-9d3e-c7d4f0bb3daf",
"name": "github_rate_limit_error",
"type": "n8n-nodes-base.stopAndError",
"position": [
768,
976
],
"parameters": {
"errorMessage": "={{ 'GitHub bloque\u00f3 la solicitud por l\u00edmite de API (HTTP ' + ($json.statusCode ?? 'desconocido') + '). Retry-After: ' + ($json.headers?.['retry-after'] ?? 'no informado') + '; reset: ' + ($json.headers?.['x-ratelimit-reset'] ?? 'no informado') + '. Detalle: ' + ($json.body?.message ?? $json.message ?? 'sin detalle') }}"
},
"typeVersion": 1
},
{
"id": "c046ea6f-f1c8-4243-b388-73ae51c1d235",
"name": "github_permissions_error",
"type": "n8n-nodes-base.stopAndError",
"position": [
768,
1184
],
"parameters": {
"errorMessage": "={{ 'GitHub rechaz\u00f3 la solicitud por permisos insuficientes (HTTP ' + ($json.statusCode ?? 'desconocido') + '). Revisa el acceso al repositorio y los permisos de la credencial. Detalle: ' + ($json.body?.message ?? $json.message ?? 'sin detalle') }}"
},
"typeVersion": 1
},
{
"id": "69e15afc-f3da-44e5-9b13-40a7ec739342",
"name": "github_unexpected_error",
"type": "n8n-nodes-base.stopAndError",
"position": [
768,
1424
],
"parameters": {
"errorMessage": "={{ 'Respuesta inesperada al verificar el repositorio en GitHub (HTTP ' + ($json.statusCode ?? 'desconocido') + '). Detalle: ' + ($json.body?.message ?? $json.message ?? 'sin detalle') }}"
},
"typeVersion": 1
},
{
"id": "49b2a515-3bf2-4bab-a68f-897942b4235b",
"name": "github_connectivity_error",
"type": "n8n-nodes-base.stopAndError",
"position": [
480,
704
],
"parameters": {
"errorMessage": "={{ 'No fue posible conectar con GitHub. Revisa DNS, conectividad y tiempos de espera. Detalle: ' + ($json.error?.message ?? $json.error ?? $json.message ?? 'sin detalle') }}"
},
"typeVersion": 1
},
{
"id": "7b48899f-ffc6-47f0-824f-cd488d83e336",
"name": "Switch",
"type": "n8n-nodes-base.switch",
"position": [
736,
352
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "repo_found",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "e1198b4f-3f27-46a9-8e7f-8eb0ff5d4ef4",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ Number($json.statusCode) >= 200 && Number($json.statusCode) < 300}}",
"rightValue": ""
}
]
},
"renameOutput": true
},
{
"outputKey": "repo_not_found",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "dad8ca11-3ac8-49c6-b84d-83cacc3b7b06",
"operator": {
"type": "number",
"operation": "equals"
},
"leftValue": "={{ Number($json.statusCode) }}",
"rightValue": 404
}
]
},
"renameOutput": true
},
{
"outputKey": "auth_error",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "d55d8170-fe0f-46a8-b607-c96c52bce909",
"operator": {
"type": "number",
"operation": "equals"
},
"leftValue": "={{ Number($json.statusCode) }}",
"rightValue": 401
}
]
},
"renameOutput": true
},
{
"outputKey": "rate_limit_error",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "edf6563e-69f9-46b6-98fb-909ccc67192a",
"operator": {
"type": "number",
"operation": "equals"
},
"leftValue": "={{ Number($json.statusCode) }}",
"rightValue": 429
}
]
},
"renameOutput": true
},
{
"outputKey": "permission_error",
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "8f3c3686-7592-429e-b07a-2b6fdabdbc67",
"operator": {
"type": "number",
"operation": "equals"
},
"leftValue": "={{ Number($json.statusCode) }}",
"rightValue": 403
}
]
},
"renameOutput": true
}
]
},
"options": {
"fallbackOutput": "extra",
"renameFallbackOutput": "unexpected_error"
}
},
"typeVersion": 3.4
},
{
"id": "45ded705-07c1-49e4-81d1-fa35dc411c67",
"name": "Workflow Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2544,
176
],
"parameters": {
"color": 5,
"width": 1008,
"height": 546,
"content": "## Workflow Overview\n\nThis workflow backs up every non-archived n8n workflow as a readable JSON file in the root of a GitHub repository. It detects new, modified, renamed, and deleted workflows and synchronizes the repository accordingly.\n\n### Why is the semantic SHA stored in the filename?\n\nEach backup uses the structure `<workflow_id><separator><semantic_sha><separator><workflow_name>.json`.\n\nInstead of downloading every existing JSON file from GitHub on each execution, the workflow:\n\n1. Builds a deterministic SHA-256 from the workflow's behavior-relevant data: `nodes`, `connections`, and `settings`.\n2. Lists the repository files and extracts the workflow ID, semantic SHA, and name directly from each filename.\n3. Uploads a new file only when the semantic SHA or workflow name changed.\n\nThis reduces GitHub API requests, transferred data, memory usage, and execution time, especially when backing up many workflows.\n\n**Tradeoff:** because existing JSON files are not downloaded, manual changes made directly to their contents in GitHub are not detected while the semantic SHA stored in the filename remains unchanged.\n"
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"timezone": "America/Guayaquil",
"binaryMode": "separate",
"callerPolicy": "workflowsFromSameOwner",
"timeSavedMode": "fixed",
"availableInMCP": false,
"executionOrder": "v1",
"executionTimeout": 300
},
"versionId": "6180710c-e470-465d-bbe9-675545770129",
"nodeGroups": [],
"connections": {
"Switch": {
"main": [
[
{
"node": "list_files",
"type": "main",
"index": 0
}
],
[
{
"node": "create_repo",
"type": "main",
"index": 0
}
],
[
{
"node": "github_authentication_error",
"type": "main",
"index": 0
}
],
[
{
"node": "github_rate_limit_error",
"type": "main",
"index": 0
}
],
[
{
"node": "github_permissions_error",
"type": "main",
"index": 0
}
],
[
{
"node": "github_unexpected_error",
"type": "main",
"index": 0
}
]
]
},
"config": {
"main": [
[
{
"node": "get_workflows",
"type": "main",
"index": 0
}
]
]
},
"renamed": {
"main": [
[
{
"node": "loop_over_renamed",
"type": "main",
"index": 0
}
]
]
},
"list_files": {
"main": [
[
{
"node": "exists_files",
"type": "main",
"index": 0
}
],
[
{
"node": "workflows_init",
"type": "main",
"index": 0
}
]
]
},
"create_repo": {
"main": [
[
{
"node": "workflows_init",
"type": "main",
"index": 0
}
]
]
},
"verify_repo": {
"main": [
[
{
"node": "Switch",
"type": "main",
"index": 0
}
],
[
{
"node": "github_connectivity_error",
"type": "main",
"index": 0
}
]
]
},
"create_files": {
"main": [
[
{
"node": "loop_new_items",
"type": "main",
"index": 0
}
]
]
},
"delete_files": {
"main": [
[
{
"node": "loop_over_deleted",
"type": "main",
"index": 0
}
]
]
},
"exists_files": {
"main": [
[
{
"node": "files_analizer",
"type": "main",
"index": 0
}
],
[
{
"node": "workflows_init",
"type": "main",
"index": 0
}
]
]
},
"get_file_sha": {
"main": [
[
{
"node": "verify_repo",
"type": "main",
"index": 0
}
]
]
},
"get_workflows": {
"main": [
[
{
"node": "get_not_archived",
"type": "main",
"index": 0
}
]
]
},
"new_workflows": {
"main": [
[
{
"node": "loop_over_created",
"type": "main",
"index": 0
}
]
]
},
"files_analizer": {
"main": [
[
{
"node": "changed_content_same_name",
"type": "main",
"index": 0
},
{
"node": "renamed",
"type": "main",
"index": 0
},
{
"node": "new_workflows",
"type": "main",
"index": 0
},
{
"node": "deleted_workflows",
"type": "main",
"index": 0
}
]
]
},
"loop_new_items": {
"main": [
[
{
"node": "files_created",
"type": "main",
"index": 0
}
],
[
{
"node": "create_files",
"type": "main",
"index": 0
}
]
]
},
"workflows_init": {
"main": [
[
{
"node": "loop_new_items",
"type": "main",
"index": 0
}
]
]
},
"create_new_files": {
"main": [
[
{
"node": "loop_over_created",
"type": "main",
"index": 0
}
]
]
},
"exists_workflows": {
"main": [
[
{
"node": "get_file_sha",
"type": "main",
"index": 0
}
],
[
{
"node": "do_nothing",
"type": "main",
"index": 0
}
]
]
},
"get_not_archived": {
"main": [
[
{
"node": "exists_workflows",
"type": "main",
"index": 0
}
]
]
},
"deleted_workflows": {
"main": [
[
{
"node": "loop_over_deleted",
"type": "main",
"index": 0
}
]
]
},
"loop_over_created": {
"main": [
[
{
"node": "files_created1",
"type": "main",
"index": 0
}
],
[
{
"node": "create_new_files",
"type": "main",
"index": 0
}
]
]
},
"loop_over_deleted": {
"main": [
[
{
"node": "files_deleted",
"type": "main",
"index": 0
}
],
[
{
"node": "delete_files",
"type": "main",
"index": 0
}
]
]
},
"loop_over_renamed": {
"main": [
[
{
"node": "files_renamed",
"type": "main",
"index": 0
}
],
[
{
"node": "create_renamed_files",
"type": "main",
"index": 0
}
]
]
},
"loop_over_updated": {
"main": [
[
{
"node": "files_updated",
"type": "main",
"index": 0
}
],
[
{
"node": "create_updated_file",
"type": "main",
"index": 0
}
]
]
},
"create_updated_file": {
"main": [
[
{
"node": "delete_outdated_file",
"type": "main",
"index": 0
}
]
]
},
"create_renamed_files": {
"main": [
[
{
"node": "delete_previous_files",
"type": "main",
"index": 0
}
]
]
},
"delete_outdated_file": {
"main": [
[
{
"node": "loop_over_updated",
"type": "main",
"index": 0
}
]
]
},
"delete_previous_files": {
"main": [
[
{
"node": "loop_over_renamed",
"type": "main",
"index": 0
}
]
]
},
"changed_content_same_name": {
"main": [
[
{
"node": "loop_over_updated",
"type": "main",
"index": 0
}
]
]
},
"When clicking \u2018Execute workflow\u2019": {
"main": [
[
{
"node": "config",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
githubApigithubOAuth2Apin8nApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow backs up all non-archived n8n workflows to a GitHub repository as JSON files, using a deterministic semantic SHA in each filename to detect new, updated, renamed, and deleted workflows and keep the repository in sync. Runs when you manually execute the workflow and…
Source: https://n8n.io/workflows/17352/ — 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.
This workflow provides everything you need to package and deploy multiple workflows from a single workflow you distribute.
[n8n] Advanced URL Parsing and Shortening Workflow - Switchy.io Integration. Uses splitInBatches, stickyNote, httpRequest, html. Event-driven trigger; 56 nodes.
[](https://youtu.be/c7yCZhmMjtI)
Git Commit. Uses github, n8n, formTrigger, httpRequest. Event-driven trigger; 34 nodes.
Remixed Backup your workflows to GitHub from Solomon's work. Check out his templates.