Back up n8n workflows to GitHub and get alerts on failed runs

A nightly n8n workflow that commits every workflow's JSON to a private GitHub repo, plus an error workflow that posts failures to Slack with a link.

· ranx.cloud

Illustration of rolled blueprints being filed into an open wooden chest, with a lantern glowing beside it

This is for whoever runs an n8n instance that other people now depend on. By the end you have two small workflows: one that commits the JSON of every changed workflow to a private GitHub repository each night, so you have history and a way back after a bad edit or a lost instance, and one that posts to Slack whenever any production workflow fails, with a link straight to the failed execution. Both use only n8n's web UI and built-in nodes.

Why bother

n8n keeps workflows, credentials and execution history in its database. On a typical small self-hosted setup that's a SQLite file on one disk. If the disk goes, or someone deletes the instance, or a colleague "cleans up" a workflow on Friday afternoon, there's nothing to go back to unless you made a copy. Many hosting setups, including ranx.cloud, don't take backups for you, so this is on you.

n8n has built-in Git sync (the Push to git menu item), but source control is a paid Enterprise feature. The API-plus-GitHub approach below works on the free self-hosted edition.

For a one-off copy you don't need any of this: open a workflow, open the workflow menu (the ... in the canvas header) and choose Export JSON. Older docs call it Download. To restore, the same menu has Import → From file.

What a workflow backup does and doesn't contain

The exported JSON has the nodes, their parameters, connections, settings and tags. For each credential a node uses, it has the credential's name and ID, not the secret. Secrets are encrypted in the database with the instance's encryption key, and you can't export them from the UI.

So keep a separate, private list of every credential: what it's called in n8n, which service and account it belongs to, and where the key or OAuth client was created ("Stripe live key n8n alerts, dashboard.stripe.com → API keys"). After a restore into a new instance you recreate the credentials and re-select them in each node. Most nodes show a red warning until you do.

Part 1: nightly backup to GitHub

Schedule Trigger → n8n (Workflow: Get Many) → Filter (changed in last 25h)
  → GitHub (File: Edit) ─error output→ GitHub (File: Create)

1. Create the GitHub repo and token

  1. Create a private repository, for example n8n-backups, and initialize it with a README so the default branch exists.
  2. Create a fine-grained personal access token at github.com/settings/personal-access-tokens/new. Repository access: Only select repositories → n8n-backups. Repository permissions: Contents → Read and write. Set an expiry and a calendar reminder for it.
  3. In n8n, create a GitHub API credential. Leave Github Server as https://api.github.com, put your GitHub username in User and the token in Access Token.

Workflow JSON can contain things you'd rather not publish: internal URLs, email addresses in Slack messages, and, if someone pasted a key straight into an HTTP Request header instead of using a credential, that key. Keep the repo private.

2. Create an n8n API key

  1. In n8n, go to Settings → n8n API and click Create an API key.
  2. Give it a label (github-backup) and an expiration. Copy the key; it's shown once.
  3. Create an n8n API credential. API Key: the key. Base URL: your instance URL plus /api/v1, for example https://n8n.example.com/api/v1.

The n8n API is part of the free self-hosted edition and on by default. On n8n Cloud it's not available during the free trial.

3. Schedule Trigger

Trigger Interval:   Days
Days Between Triggers: 1
Trigger at Hour:    3am

On the trigger's Settings tab, set If Execution Is Missed to Run the Most Recent Missed Execution and the grace period to 3600. If the instance is restarting at 3:00, the backup then runs late instead of being skipped until the next night.

4. n8n node: list workflows

Resource:   Workflow
Operation:  Get Many
Return All: on

Each output item is one workflow with its id, name, nodes, connections, settings and updatedAt.

5. Filter: only what changed

{{ DateTime.fromISO($json.updatedAt) > $now.minus({ hours: 25 }) }}   is true

Without this, the workflow rewrites every file every night and the commit history fills with noise. The 25 hours gives an hour of slack for a late run. For the very first run, disable this node (right-click → Deactivate) so everything gets committed once, then enable it again.

6. GitHub: update the file, create it if it's new

GitHub's API needs the current file's SHA to update a file and rejects a create if the file already exists. The GitHub node's Edit operation looks the SHA up for you, but fails if the file doesn't exist yet. So try Edit first and send failures to Create.

First GitHub node, named Update file:

Resource:         File
Operation:        Edit
Repository Owner: <your user or org>
Repository Name:  n8n-backups
File Path:        workflows/{{ $json.id }}.json
Binary File:      off
File Content:     {{ JSON.stringify($json, null, 2) }}
Commit Message:   backup: {{ $json.name }} ({{ $json.id }})

In its Settings tab, set On Error to Continue (using error output). The node now has a second, error output.

Second GitHub node on that error output, named Create file:

Resource:         File
Operation:        Create
Repository Owner: <your user or org>
Repository Name:  n8n-backups
File Path:        workflows/{{ $('Filter').item.json.id }}.json
Binary File:      off
File Content:     {{ JSON.stringify($('Filter').item.json, null, 2) }}
Commit Message:   add: {{ $('Filter').item.json.name }}

The Create node refers back to the Filter node by name because the items on an error output carry the error, not necessarily the original workflow data.

File names use the workflow ID rather than the name. Names change and can contain slashes; IDs don't. Search the repo for "name": "Invoice intake" when you need to find one.

7. Run it once and publish

Click Execute workflow with the Filter disabled, check the repo, re-enable the Filter, Publish.

Restoring

From the repo, download workflows/<id>.json, then in n8n create a new workflow, open the workflow menu and choose Import → From file. Re-select credentials in any node that shows a warning, check the trigger, and publish. If the old workflow still exists, archive it first so you don't end up with two published copies listening on the same webhook path.

Part 2: an error workflow that posts to Slack

An error workflow is a normal workflow that starts with an Error Trigger. You point other workflows at it in their settings, and when a production execution of one of them fails, n8n runs it with details of the failure. The n8n error handling docs cover the mechanics.

1. Build it

Create a workflow called Error alerts with an Error Trigger followed by a Slack node (Message → Send, to #automation-alerts, Simple Text Message):

:x: *{{ $json.workflow.name }}* failed
Node: {{ $json.execution.lastNodeExecuted }}
Error: {{ $json.execution.error.message }}
{{ $json.execution.url }}

For the Slack credential: api.slack.com/apps → Create New App → From scratch, bot scope chat:write, install, paste the xoxb- token into a Slack API credential, /invite the bot to the channel.

execution.url is a link into your instance's editor; it's built from the instance's public URL, so it works from Slack on your phone.

Save it. A workflow that starts with an Error Trigger doesn't need to be published.

2. Point workflows at it

In each workflow you care about, open the workflow menu → Settings, set Error Workflow (to notify when this one errors) to Error alerts, and save. Do this for the backup workflow from Part 1 too.

You can't test it with a manual run; the Error Trigger only fires for automatic (production) executions. To test, publish a throwaway workflow with a Schedule Trigger every minute and a Stop and Error node, point it at Error alerts, wait a minute, then unpublish it.

3. Know what it won't catch

  • Trigger failures before the workflow runs. If, say, a Gmail credential expires and the trigger can't poll, the error workflow gets a different payload with trigger.error instead of execution, and no execution URL. Add a fallback in the message: {{ $json.execution?.error?.message ?? $json.trigger?.error?.message }}.
  • Runs that never started. A schedule missed while the instance was down isn't a failure; nothing runs, nothing alerts. Check Executions after any downtime.
  • Executions cut off by a restart. n8n marks them as crashed when it starts again. They show up in Executions with that status; don't count on an alert for them. If the last three executions of a workflow all crashed, n8n unpublishes the workflow automatically (and tries to email the owner, which only works if the instance has email set up). On an instance that restarts occasionally, a workflow that silently stopped is worth a monthly look at the workflow list.

Costs and limits

No LLM here. GitHub's REST API allows 5,000 requests per hour for a token (GitHub rate limits); a nightly run with a few dozen changed workflows uses a few dozen, plus one SHA lookup each.

Execution history is pruned by default after 14 days or 10,000 executions, whichever comes first, so the backup repo is your only long-term history of the workflows themselves. It doesn't back up execution data, and on a small disk you don't want it to.

A few other things worth knowing on a hosted instance

These are n8n 2.x defaults that matter when you can't change the server's environment variables:

  • Execute Command and Local File Trigger nodes are disabled by default (NODES_EXCLUDE). They don't appear in the nodes panel.
  • Code node runs JavaScript in n8n's built-in task runner. Importing npm modules or Node built-ins like fs isn't allowed unless the instance was started with NODE_FUNCTION_ALLOW_EXTERNAL / NODE_FUNCTION_ALLOW_BUILTIN. Python in the Code node needs a Python runtime that the standard n8nio/n8n image doesn't include, so expect it to fail with a "Python 3 is missing" error; write the logic in JavaScript.
  • Community nodes can be installed from Settings → Community nodes → Install a community node by npm package name. Both verified and unverified packages are allowed by default. Installing one runs third-party code inside your instance with access to your credentials, so read its repository first. The n8n docs on GUI installation cover the risks and the update flow.

What goes wrong

The n8n node returns 401. The API key expired (you set an expiration) or was deleted. Create a new one and update the credential.

The n8n node times out or can't connect. The Base URL is wrong: it must include /api/v1 and use https://. If your instance can't reach its own public hostname, try http://localhost:5678/api/v1, which talks to the same n8n process directly.

GitHub returns 404 on Edit for every file. That's expected on the first run (the files don't exist) and the error output sends them to Create. If Create also returns 404, the token can't see the repo: check the fine-grained token's repository selection and that the Contents permission is Read and write.

GitHub returns 409 or 422. Two runs overlapped and the SHA changed between lookup and write, or Create ran for a file that exists. Re-run; if it persists, check you don't have two copies of the backup workflow published.

The backup quietly stopped weeks ago. The GitHub token expired. Fine-grained tokens have an expiry by design. Because the backup workflow points at Error alerts, you get a Slack message on the first failed night, as long as you did step 2 of Part 2 for it.