~ / blog / github-backup-n8n
misc2026-03-17

Back Up Your n8n Workflows to GitHub — Automatically

Organize every workflow by tag, name files after the workflow, and never lose a working automation again.

#N8n#GitHub#Automations#Backup#Homelab

Built this because I kept breaking workflows and had no way to roll back. Now every workflow is versioned in GitHub automatically.


The Problem #

n8n doesn't have built-in version control. You break something, hit save, and the previous working version is gone. There's no undo past the last state.

The naive fix is to export workflows manually. That works once. Then you forget to do it for three weeks and the next time you need a rollback, you're out of luck.

The actual fix: a workflow that backs up all your other workflows to GitHub on demand. Organized by tag, named after the workflow, with GitHub's full commit history as version control.


How It Works #

  Manual Trigger
       │
       v
  Config Node          repo_owner · repo_name
       │
       v
  Get All Workflows    <── n8n API
       │
       v
  Prepare File Paths
       │  · skip archived
       │  · sanitize name   spaces -> _
       │  · first tag -> folder
       │  · fallback: workflows/
       │
       v
  Loop Over Items  ──────────────────────┐
       │                                 │
       v                                 │
  GET /contents/{path}   GitHub API      │
       │                                 │
       v                                 │
  sha = null?                            │
       │                                 │
       +── yes ->  PUT  (create)         │
       │                │                │
       +── no  ->  PUT  (update + sha)   │
                        │                │
                        └────────────────┘

Every workflow gets checked against GitHub before writing. If the file already exists, it gets updated in-place with the correct sha. If it's new, it gets created. GitHub keeps the full commit history — every version of every workflow is recoverable.


Repo Structure #

Workflows are organized by their first tag. No tag means they land in workflows/.

your-repo/
│
├── workflows/
│   └── Discord_Bot_Command.json
│
├── CTI/
│   ├── IOC_Enrichment.json
│   └── Threat_Feed_Sync.json
│
├── Homelab/
│   ├── Server_Monitor.json
│   └── Telegram_Bot_Dispatcher.json
│
└── README.md

Filenames are sanitized: spaces become underscores, special characters get stripped, - separators collapse into a single underscore.

Telegram Bot - Subworkflow - Rollo up
→ Telegram_Bot_Subworkflow_Rollo_up.json

The Workflow #

Config Node #

A simple Set node at the start. Two fields:

repo_owner   →   your GitHub username
repo_name    →   the target repo

Everything downstream reads from this node, so you only have to change one place if you rename the repo.

Prepare File Paths #

This is where the logic lives. A Code node that runs over all workflows returned by the n8n API:

const items = $input.all();
const config = $('Config').first().json;
const result = [];

for (const item of items) {
  const workflow = item.json;

  if (workflow.isArchived) continue;

  const safeName = workflow.name
    .replace(/ - /g, '_')
    .replace(/ /g, '_')
    .replace(/[^a-zA-Z0-9_]/g, '')
    .replace(/_+/g, '_')
    .replace(/^_+|_+$/g, '');

  let folder = 'workflows';
  if (workflow.tags && workflow.tags.length > 0) {
    const firstTag = workflow.tags[0].name || workflow.tags[0];
    folder = firstTag
      .replace(/ - /g, '_')
      .replace(/ /g, '_')
      .replace(/[^a-zA-Z0-9_]/g, '')
      .replace(/_+/g, '_')
      .replace(/^_+|_+$/g, '');
  }

  result.push({
    json: {
      workflowId: workflow.id,
      workflowName: workflow.name,
      safeName,
      folder,
      filePath: `${folder}/${safeName}.json`,
      fileContent: JSON.stringify(workflow, null, 2),
      repo_owner: config.repo_owner,
      repo_name: config.repo_name
    }
  });
}

return result;

Archived workflows are skipped. Everything else gets a clean file path and the full workflow JSON as content.

Loop + GitHub API #

After Prepare File Paths, a Split In Batches node (batch size 1) loops over each workflow individually.

For each workflow:

  1. GET the file from GitHub to check if it exists and grab the sha
  2. A Code node checks the response — sha = null means new file, otherwise it exists
  3. PUT to GitHub — create or update depending on the result

The GET and PUT both go directly to the GitHub REST API via HTTP Request nodes. The GitHub node in n8n doesn't expose sha correctly for updates, so raw HTTP is the cleaner approach here.

GET:

GET https://api.github.com/repos/{owner}/{repo}/contents/{path}
Authorization: Bearer <PAT>

PUT (create):

{
  "message": "backup: My Workflow (new)",
  "content": "<base64 encoded JSON>"
}

PUT (update):

{
  "message": "backup: My Workflow (updated)",
  "content": "<base64 encoded JSON>",
  "sha": "<sha from GET response>"
}

Setup #

1. Create a GitHub repo

Initialize it with a README so it's not empty — the API behaves differently on completely empty repos.

2. Generate a PAT

GitHub → Settings → Developer settings → Personal access tokens → Fine-grained

Required permissions:

Contents   →   Read and write
Metadata   →   Read-only  (auto)

3. Set up credentials in n8n

Create an HTTP Header Auth credential:

Name:   Authorization
Value:  Bearer <your token>

Use this for both the GET and PUT nodes.

4. Configure the workflow

Open the Config node, set repo_owner and repo_name. Run it manually once to do the initial backup. After that, run it whenever you want a snapshot.


Notes #

  • Only the first tag determines the folder. Multi-tag workflows don't get duplicated.
  • Archived workflows are skipped entirely.
  • Nothing gets deleted from GitHub. Removing a workflow from n8n doesn't remove it from the repo.
  • GitHub's commit history is your version control. Every run that changes a file creates a new commit.
  • The workflow backs itself up too.

← back to blog