← All posts
Sm0k · CTI Analyst & Engineer
[n8n]··4 min read

Back Up Your n8n Workflows to GitHub — Automatically

Export your n8n workflows to a private GitHub repository, organize them by tag, and keep a readable history of every change.

#n8n#GitHub#automation#backup#homelab

I built this because I kept changing working automations without keeping a reliable copy. Now every workflow is exported to a private GitHub repository with a readable change history.


The Problem

n8n offers Git-based source control on Business and Enterprise plans. For a self-hosted Community instance, however, there is no equivalent Git workflow built into the editor. Once you overwrite or delete a workflow, recovering an earlier saved definition is not as simple as checking out a commit.

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.

My practical fix is a workflow that exports all other workflows to GitHub on demand. Files are organized by tag and named after the workflow; GitHub then provides the history and diffs.

This is an export of workflow definitions, not a complete n8n instance backup. Credentials, execution history, users, settings, and the database still need their own backup strategy.


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 is updated with its current blob sha. If it is new, it is created without one. GitHub keeps a commit for every changed export, so earlier workflow definitions remain available in the repository history.


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 - Rollup
→ Telegram_Bot_Subworkflow_Rollup.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 Loop Over Items node (formerly Split In Batches) with a batch size of 1 processes each workflow serially. Serial writes matter here: parallel updates to the same branch can conflict.

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. Using the REST API makes the create-versus-update behavior explicit and gives direct access to the file sha required for updates.

Configure the GET node to continue on a 404 response. A missing file is expected on the first run and should enter the create branch rather than stop the workflow.

GET:

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

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

Make the repository private unless you intentionally want workflow structure, tags, credential references, webhook paths, and other configuration details to be public. Initialize it with a README so the default branch already exists.

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. Restrict the fine-grained token to this repository and grant only Contents: Read and write.

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.
  • Workflow exports can contain sensitive metadata even though credential secrets aren't included. Keep the repository private and review the first export before relying on automation.
  • Test a restore before calling any backup strategy complete. An export you have never restored is only an assumption.

References


← back to blog