n8n workflows are JSON code that deserves to be treated like regular code. In this episode we bring workflows into Git, automate deployment through CI/CD pipelines, and add a testing layer so every change is verified before it touches production.

In episode 17 you built custom nodes and installed extensions for needs not covered by built-in nodes. Technically your workflows are mature — but there's still one big gap: workflow changes are undocumented and uncontrolled. If two people edit the same workflow on the production instance, who's responsible when it suddenly breaks? Which change caused it?
The answer is bringing workflows into the software development lifecycle that's already common: version control, deployment pipelines, and testing. In this episode we discuss:
Every n8n workflow is essentially one JSON document. It contains a nodes array describing each node — type, position, parameters, and credentials used — as well as a connections array storing how the nodes connect to each other.
Because the format is text-based, workflows can be diffed, reviewed, and versioned like regular code files:
{
"name": "Sales Lead Enrichment",
"nodes": [
{
"id": "abc-123",
"name": "Webhook",
"type": "n8n-nodes-base.webhook",
"position": [0, 0]
},
{
"id": "def-456",
"name": "HTTP Request",
"type": "n8n-nodes-base.httpRequest",
"parameters": { "url": "https://api.example.com/enrich" }
}
],
"connections": {
"Webhook": { "main": [[{ "node": "HTTP Request", "type": "main", "index": 0 }]] }
}
}Read it carefully: type determines which node is used, parameters stores the configuration, and connections maps the data flow between nodes. Since all of this is text, a single changed line in a pull request is enough to show "remove this node, add that node".
Info
In the default export, credential fields only store reference IDs, not their secret values. This benefits repository security — but it means the restore process must supply the actual credentials from a vault, not from Git.
To bring workflows into Git, use n8n's built-in export command. Point it at a project folder already initialized with Git:
n8n export:workflow --all --output=./workflows --pretty --separate
git add workflows/
git commit -m "chore: sync workflow ke repository"The --separate flag makes each workflow its own JSON file inside the workflows folder, so each file's history can be traced separately. --pretty makes the JSON easy to read during code review. After this first commit, every change in the editor can be re-synced and committed with a message explaining why.
A few habits that make versioning healthier:
fix: perbaiki mapping field di lead workflow or feat: tambah cabang approval.credentials/ to .gitignore if any credential files get synced, and make the repository contain only definitions, not secrets.--separate mode so merge conflicts don't roll across the whole instance.Credentials are the one thing that must never be committed. n8n stores them encrypted in the database with the N8N_ENCRYPTION_KEY key. For backup or migration, export credentials separately and store them in a vault:
n8n export:credential --all --output=./credentials --decryptedThe exported files contain secrets in decrypted form — never put them in Git. Store them in a secret manager like Vault or AWS Secrets Manager, then import them back when restoring. In the repository, just reference the credential names so developers know which connection is needed, without the actual values.
A good repository separates environments. One common pattern: a workflows folder contains the source .workflow.json files, while environment-specific values like base URLs or staging API keys are injected via environment variables at runtime.
A structure commonly used by production teams:
repo-automasi/
├── workflows/ # export n8n:workflow, satu file per workflow
│ ├── sales-lead-enrichment.workflow.json
│ └── order-fulfillment.workflow.json
├── credentials/ # tidak di-commit, hanya dipakai saat restore
├── tests/ # test definitions untuk n8n test
├── scripts/
│ └── deploy.sh # wrapper import untuk environment tertentu
└── .github/
└── workflows/ # pipeline CI/CDWith this structure, the deploy stage is simple: import all workflow files into the target instance. The script below reads the N8N_BASE_URL and N8N_USER_API_KEY env vars so it points to the correct environment:
n8n import:workflow --separate --input=./workflows
n8n import:credential --all --input=./credentialsThe import upserts workflows by name or ID, so workflows no longer in the repository won't be deleted automatically — make that deletion part of the deploy checklist, not improvisation.
With all versions in Git, the CI/CD pipeline just repeats the same steps on every push to the production branch. The following GitHub Actions workflow deploys to staging on a push to main:
name: Deploy Workflows
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Pasang n8n CLI
run: npm install -g n8n
- name: Validasi JSON workflow
run: |
for f in workflows/*.json; do jq empty "$f"; done
- name: Import workflow ke n8n
run: n8n import:workflow --separate --input=./workflows
env:
N8N_BASE_URL: ${{ secrets.N8N_BASE_URL }}
N8N_USER_API_KEY: ${{ secrets.N8N_USER_API_KEY }}What's worth noting:
jq empty catches broken files before import — cheap, fast, and saves you from errors mid-pipeline.tags trigger with a v* pattern.Teams using queue mode with several workers (discussed in episode 15) keep the same pipeline; the difference is that after a successful import, add a worker container restart step so the latest definitions are picked up.
Automated deployment without testing only speeds up breakage. Two layers worth applying:
Layer one: static validation. Check the JSON structure, make sure no node points to a node that doesn't exist, and ensure every credential ID has a match. This can run as a lightweight script in the pipeline:
n8n testOn recent n8n versions, the n8n test command runs the test scenarios you define in the tests folder, complete with input fixtures and assertions against output. It works like unit tests: any workflow change that breaks expectations fails the pipeline before going to production.
Layer two: post-deploy smoke tests. After a successful import, execute the workflow once with dummy data and make sure the status is success. You can trigger a test webhook or use a one-off execution via the CLI:
n8n execute --id=abc123def456If the exit code isn't zero, the pipeline is considered failed — an alarm to the team before the workflow is genuinely used in production.
Warning
Don't test with real production data. Prepare fixtures and isolated staging webhooks. Workflows that touch email, SMS, or paid external APIs should use dry-run mode or mock services so tests don't incur costs and side effects.
In this episode workflows changed from "visual creations in the editor" into assets managed like code:
With this lifecycle, the question "who changed what and when" is no longer a mystery. In episode 19 we discuss operational readiness: compiling runbooks for incidents and recovery, setting SLAs and workflow ownership, as well as backup, restore, and disaster recovery strategies. See you there!