// Gotowe workflow

Kopia workflow na GitHuba

▸ // rozdziały lekcji

Kopia workflow na GitHuba - Wolniś: „Kopia jest. Śpię spokojnie”

Skasujesz węzeł, zapiszesz, zamkniesz. Tydzień później orientujesz się, że był potrzebny. n8n ma historię wersji (trzy kropki obok nazwy workflow, Version history), ale w darmowej edycji sięga ona 1 dnia. Sprawdziłem. Wczorajszą wersję jeszcze odzyskasz, zeszłotygodniowej już nie.

Ten workflow robi kopię, która nie wygasa. Raz w tygodniu pobiera przez API wszystkie twoje automatyzacje i zapisuje każdą jako plik JSON w repozytorium na GitHubie. Na darmowej edycji to jedyna trwała historia, jaką masz.

Co robi ten workflow

  1. Schedule Trigger - uruchamia workflow w każdy poniedziałek o 6:30.
  2. Manual Test GitHub - drugi start, ręczny, do testów przyciskiem Execute workflow.
  3. Ustawienia - trzy pola: MAIN_FOLDER (katalog w repo), REPO_OWNER (twój login na GitHubie), REPO_NAME (nazwa repozytorium).
  4. n8n - api - HTTP Request pobiera listę wszystkich workflow z API twojej instancji.
  5. Process for GitHub Upload - węzeł Code przygotowuje z każdego workflow plik JSON gotowy do importu, jego nazwę i ścieżkę w repo.
  6. Filter - przepuszcza tylko workflow, które nie są zarchiwizowane.
  7. Loop Over Items - puszcza pliki po jednym.
  8. Upload to GitHub Repo1 - węzeł GitHub zakłada plik w repozytorium, na gałęzi main.
  9. GitHub Upload Success1 - węzeł Code zbiera wynik zapisu: ścieżkę, adres pliku i identyfikator commita. Potem pętla bierze następny plik.

Cała kanwa - dwa wyzwalacze wchodzące w Ustawienia i pętla z węzłem GitHub

Czego potrzebujesz

  • klucza API n8n. Otwórz Settings w lewym dolnym rogu, potem n8n API, i utwórz klucz. W Scopes wystarczy Read only, bo workflow tylko czyta listę workflow. Klucz ma datę wygaśnięcia, zapisz ją sobie,
  • tokenu GitHuba z prawem zapisu do repozytorium,
  • prywatnego repozytorium na kopie. Najprościej załóż je z plikiem README, wtedy gałąź main na pewno istnieje.

Poświadczenia zakładasz w Overview, zakładka Credentials. Oba krok po kroku pokazuję w lekcjach Credentials n8n API i Credentials GitHub.

Haczyk. Repozytorium musi być prywatne. Eksport workflow nie zawiera haseł ani tokenów, ale zawiera adresy twoich instancji, identyfikatory baz, kanałów i webhooków. Do tego ten workflow kopiuje przypięte dane (pin data), a w nich bywają prawdziwe maile i dane klientów. Na publicznym repo to gotowa mapa twojej infrastruktury.

Jak uruchomić

  1. Zaimportuj plik z tej lekcji (jak importować workflow).
  2. W węźle n8n - api zamień https://twoja-instancja.n8n.cloud na adres swojej instancji, zostaw końcówkę /api/v1/workflows. Wybierz poświadczenie typu n8n API z twoim kluczem.
  3. W węźle Ustawienia wpisz swój login w REPO_OWNER i nazwę repozytorium w REPO_NAME. W pliku jest zaślepka TWOJ_LOGIN_GITHUB - bez podmiany workflow skończy się błędem.
  4. W węźle Upload to GitHub Repo1 wybierz poświadczenie GitHuba.
  5. Kliknij Execute workflow i sprawdź w repozytorium, czy pojawił się katalog z dzisiejszą datą.
  6. Kliknij Publish. Bez tego harmonogram się nie odpali (pokazuję to w lekcji 2.2).

Węzeł Ustawienia z polami MAIN_FOLDER, REPO_OWNER i REPO_NAME

Co dokładnie ląduje w repo

Każde uruchomienie tworzy nowy katalog z datą. Struktura wygląda tak:

workflows/
  20260928/
    backup-n8n.json
    twoj-inny-workflow.json
    ...

Węzeł Process for GitHub Upload robi z każdego workflow plik gotowy do importu. Bierze nazwę, węzły, połączenia, ustawienia, dane statyczne, przypięte dane i tagi. Ustawia active: false, żeby workflow po imporcie nie ruszył sam. Dopisuje też sekcję backupInfo z oryginalnym ID i datą kopii.

Nazwę pliku kod bierze z nazwy workflow. Wycina wszystko poza literami a-z, cyframi, spacjami i dywizami, spacje zamienia na dywizy i przycina do 40 znaków. Opis commita to „Backup workflow: nazwa (data)".

Filter odrzuca workflow zarchiwizowane. Wszystkie pozostałe idą do repo, łącznie z tym workflow backupu.

Haczyk. Nazwa pliku gubi polskie znaki. Workflow „Źródła RSS" zapisze się jako rda-rss.json. Dwa workflow, których nazwy różnią się tylko ogonkami albo mają te same pierwsze 40 znaków, trafią pod tę samą ścieżkę. Nie sprawdzałem, co wtedy zrobi węzeł GitHub. Ma ustawione „kontynuuj przy błędzie", więc najpewniej jedna kopia przepadnie po cichu. Najprostsze obejście: nadawaj workflow nazwy, które różnią się czymś więcej niż ogonkami.

Pro-tip. Klucz API n8n wygasa w dniu, który ustawiłeś przy tworzeniu. Po tej dacie węzeł n8n - api zacznie zwracać błąd i kopie przestaną powstawać. Podepnij pod ten workflow alert błędów na Discord, to dowiesz się o tym w poniedziałek rano, a nie w dniu, w którym będziesz potrzebować kopii.

Dlaczego raz w tygodniu

Harmonogram jest ustawiony na poniedziałek, 6:30. Każde uruchomienie to pełny zestaw plików w nowym katalogu, więc codzienna kopia przy kilkudziesięciu workflow szybko zamienia repo w stertę prawie identycznych katalogów. Jeśli dłubiesz w automatyzacjach codziennie, zmień w Schedule Trigger interwał na dni. Nie podpinałbym kopii pod każdy zapis workflow, bo przy pracy nad jednym workflow wychodzi kilkadziesiąt commitów dziennie.

Data w nazwie katalogu to data w strefie UTC, bo kod liczy ją z toISOString(). Jeśli instancja ma ustawioną polską strefę czasową, uruchomienie o 6:30 daje datę tego samego dnia.

Jak przywrócić workflow z kopii

Otwórz plik na GitHubie, skopiuj zawartość i wklej na pustą kanwę albo zaimportuj plik przez menu z trzema kropkami (Import). Szczegóły w lekcji jak importować workflow. Poświadczenia podpinasz od nowa, bo kopia ich nie zawiera.

Co naprawiłem i co sprawdziłem

W starej wersji tej lekcji JSON miał w kodzie JavaScript niezakodowane znaki nowej linii. Po skopiowaniu nie dało się go wkleić do n8n. Teraz import działa, sprawdziłem na n8n 2.40.6.

Części z GitHubem nie testowałem: zapisu plików, commitów ani zachowania przy drugim uruchomieniu tego samego dnia, kiedy pliki pod tą samą ścieżką już istnieją. Pierwsze uruchomienie zrób ręcznie i obejrzyj wynik w repo, zanim zostawisz workflow samemu sobie.

Podsumowanie

Historia wersji w darmowym n8n pamięta 1 dzień. Ten workflow co poniedziałek zapisuje wszystkie twoje automatyzacje jako pliki gotowe do importu, w katalogu z datą, w prywatnym repo. Konfiguracja to klucz API n8n, token GitHuba i trzy pola w Ustawieniach.

W następnej lekcji, Aktualizacja zadań w Todoist, automatyzujemy listę zadań.

Pokaż JSON do skopiowania - kopia-workflow-na-github.json
{
  "name": "BackUp n8n",
  "nodes": [
    {
      "parameters": {},
      "id": "5a2740a5-3044-48cd-8639-f2a83894569b",
      "name": "Manual Test GitHub",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -368,
        -64
      ],
      "typeVersion": 1
    },
    {
      "parameters": {
        "jsCode": "// Process each workflow for GitHub upload\nconst workflows = $input.all()[0].json.data || [];\nconst now = new Date();\nconst dateStr = now.toISOString().slice(0, 10).replace(/-/g, ''); // Format: 20250722\n\n// Configuration - you can change these\nconst MAIN_FOLDER = $('Ustawienia').first().json.MAIN_FOLDER // Main folder name - CHANGE THIS!\nconst REPO_OWNER = $('Ustawienia').first().json.REPO_OWNER // CHANGE THIS!\nconst REPO_NAME =  $('Ustawienia').first().json.REPO_NAME// CHANGE THIS!\n\nconsole.log(`📁 Creating folder structure: ${MAIN_FOLDER}/${dateStr}/`);\nconsole.log(`📅 Date folder: ${dateStr}`);\n\nconst results = [];\n\nworkflows.forEach((workflow, index) => {\n  console.log(workflow)\n  // Clean workflow name for filename\n  const cleanName = workflow.name\n    .replace(/[^a-zA-Z0-9\\s-]/g, '')\n    .replace(/\\s+/g, '-')\n    .toLowerCase()\n    .substring(0, 40);\n\n  // Create import-ready workflow data\n  const importReadyWorkflow = {\n    name: workflow.name,\n    nodes: workflow.nodes || [],\n    connections: workflow.connections || {},\n    settings: workflow.settings || {},\n    staticData: workflow.staticData || {},\n    pinData: workflow.pinData || {},\n    tags: workflow.tags || [],\n    active: false,\n    meta: {\n      backupInfo: {\n        originalId: workflow.id,\n        backupDate: dateStr,\n        backupTimestamp: now.toISOString(),\n        source: 'n8n-github-backup'\n      }\n    }\n  };\n\n  // Create file content\n  const fileContent = JSON.stringify(importReadyWorkflow, null, 2);\n  const fileName = `${cleanName}.json`;\n  \n  // CORRECTED: Proper folder structure - Main folder -> Date subfolder -> Files\n  const filePath = `${MAIN_FOLDER}/${dateStr}/${fileName}`;\n\n  results.push({\n    json: {\n      repoOwner: REPO_OWNER,\n      repoName: REPO_NAME,\n      filePath: filePath,\n      fileName: fileName,\n      mainFolder: MAIN_FOLDER,\n      dateFolder: dateStr,\n      workflowName: workflow.name,\n      workflowId: workflow.id,\n      fileContent: fileContent,\n      fileSize: fileContent.length,\n      isArchived: workflow.isArchived,\n      commitMessage: `Backup workflow: ${workflow.name} (${dateStr})`\n    }\n  });\n});\n\nconsole.log(`📂 Will create structure:`);\nconsole.log(`   ${MAIN_FOLDER}/`);\nconsole.log(`   └── ${dateStr}/`);\nresults.forEach((item, i) => {\n  console.log(`       └── ${item.json.fileName}`);\n});\nconsole.log(`💼 Repository: ${REPO_OWNER}/${REPO_NAME}`);\nconsole.log(`📁 Prepared ${results.length} workflows for GitHub upload`);\n\nreturn results;"
      },
      "id": "e2b2357e-2a1e-47c1-952e-4a19c7b4e85a",
      "name": "Process for GitHub Upload",
      "type": "n8n-nodes-base.code",
      "position": [
        304,
        -160
      ],
      "typeVersion": 2
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "TWOJE_ID_BAZY",
              "leftValue": "={{ $json.isArchived }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2.2,
      "position": [
        528,
        -160
      ],
      "id": "746e13b2-6a2d-4838-9c66-a96739cacf98",
      "name": "Filter"
    },
    {
      "parameters": {
        "resource": "file",
        "owner": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.repoOwner }}"
        },
        "repository": {
          "__rl": true,
          "mode": "name",
          "value": "={{ $json.repoName }}"
        },
        "filePath": "={{ $json.filePath }}",
        "fileContent": "={{ $json.fileContent }}",
        "commitMessage": "={{ $json.commitMessage }}",
        "additionalParameters": {
          "branch": {
            "branch": "main"
          },
          "author": {
            "name": "n8n Backup System",
            "email": "backup@n8n.local"
          }
        }
      },
      "id": "e9cee2f9-d446-4a20-8bba-9b902b17398d",
      "name": "Upload to GitHub Repo1",
      "type": "n8n-nodes-base.github",
      "position": [
        976,
        -224
      ],
      "typeVersion": 1,
      "retryOnFail": false,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "// Log successful GitHub upload\nconst result = $input.all()[0].json;\n\nconsole.log('✅ GitHub Upload Successful!');\nconsole.log(`📁 File: ${$('Upload to GitHub Repo1').item.json.filePath}`);\nconsole.log(`💼 Repository: ${$('Upload to GitHub Repo1').item.json.repoOwner}/${$('Upload to GitHub Repo1').item.json.repoName}`);\nconsole.log(`🔗 URL: ${result.html_url}`);\nconsole.log(`📝 Commit: ${result.commit.sha}`);\n\nreturn [{\n  json: {\n    success: true,\n    fileName: $('Upload to GitHub Repo1').item.json.fileName,\n    filePath: $('Upload to GitHub Repo1').item.json.filePath,\n    commitSha: result.commit.sha,\n    fileUrl: result.html_url,\n    workflowName: $('Upload to GitHub Repo1').item.json.workflowName\n  }\n}];"
      },
      "id": "654c002a-d601-4e85-a709-1191fed3e20a",
      "name": "GitHub Upload Success1",
      "type": "n8n-nodes-base.code",
      "position": [
        1200,
        -160
      ],
      "typeVersion": 2,
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        752,
        -160
      ],
      "id": "d854626b-84f9-41da-b630-6d8c40e36202",
      "name": "Loop Over Items"
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 6,
              "triggerAtMinute": 30
            }
          ]
        }
      },
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -368,
        -256
      ],
      "id": "e09d02ce-7e1b-4bd1-8695-8812268a9d2b",
      "name": "Schedule Trigger"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "TWOJE_ID_BAZY",
              "name": "MAIN_FOLDER",
              "value": "workflows",
              "type": "string"
            },
            {
              "id": "TWOJE_ID_BAZY",
              "name": "REPO_OWNER",
              "value": "TWOJ_LOGIN_GITHUB",
              "type": "string"
            },
            {
              "id": "TWOJE_ID_BAZY",
              "name": "REPO_NAME",
              "value": "n8n-backup",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        -144,
        -160
      ],
      "id": "8b8b05f4-b8d7-4cb0-bf8b-1aff0b899af1",
      "name": "Ustawienia"
    },
    {
      "parameters": {
        "url": "https://twoja-instancja.n8n.cloud/api/v1/workflows",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "n8nApi",
        "options": {
          "response": {
            "response": {
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "ffae5f07-7486-463f-a44e-f71a27f08c53",
      "name": "n8n - api",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        80,
        -160
      ],
      "typeVersion": 4
    }
  ],
  "connections": {
    "Manual Test GitHub": {
      "main": [
        [
          {
            "node": "Ustawienia",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process for GitHub Upload": {
      "main": [
        [
          {
            "node": "Filter",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Upload to GitHub Repo1": {
      "main": [
        [
          {
            "node": "GitHub Upload Success1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "Upload to GitHub Repo1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "GitHub Upload Success1": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Ustawienia",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ustawienia": {
      "main": [
        [
          {
            "node": "n8n - api",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "n8n - api": {
      "main": [
        [
          {
            "node": "Process for GitHub Upload",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "active": false
}