Migrate to RunxBuild and earn up to $50 in hosting credit on your first deposit.

Calculate your savings
unxBuild

n8n Workflow JSON: The Structure, and How to Edit It by Hand

Sean

Platform Writer

Aug 26, 2026
8 min read

An n8n workflow is a JSON object with two significant keys: nodes, an array of node definitions with positions and parameters, and connections, a map describing what feeds into what. Understanding that structure is what lets you keep workflows in version control, edit them in bulk, and diagnose an import that will not load.

n8n Workflow JSON: The Structure, and How to Edit It by Hand

The canvas is the normal way to build a workflow, and it is good at that. But workflows are text underneath, and treating them as text unlocks the things the canvas cannot do — reviewing a change in a pull request, updating twenty nodes at once, and understanding why an imported workflow fails.

Table of contents

The minimal structure

{
  "name": "Daily Report",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [{ "triggerAtHour": 8 }]
        }
      },
      "id": "a1b2c3d4-0000-4000-8000-000000000001",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [260, 300]
    },
    {
      "parameters": {
        "url": "https://api.example.com/report",
        "options": {}
      },
      "id": "a1b2c3d4-0000-4000-8000-000000000002",
      "name": "Fetch Report",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [480, 300]
    }
  ],
  "connections": {
    "Schedule Trigger": {
      "main": [[{ "node": "Fetch Report", "type": "main", "index": 0 }]]
    }
  },
  "settings": { "executionOrder": "v1" }
}

The fields that matter on each node:

  • type — the node type identifier. Case sensitive. If this names a node your instance does not have, the workflow will not load.
  • typeVersion — the node’s parameter schema version. Nodes change their parameters between versions, and this tells n8n how to interpret them.
  • name — the display name, and critically the key used in connections. Renaming a node means updating every reference.
  • position — canvas coordinates. Cosmetic, but a workflow with everything at [0, 0] is unreadable.
  • id — a UUID, unique within the workflow.

The nesting in connections is the part people find confusing: main is the output type, the outer array is the output index, and the inner array is the list of nodes connected to that output. A node with two outputs, such as an If node, uses index 0 for true and index 1 for false.

Multiple outputs and branching

An If node’s connections show the structure clearly:

"connections": {
  "Check Status": {
    "main": [
      [{ "node": "Handle Success", "type": "main", "index": 0 }],
      [{ "node": "Handle Failure", "type": "main", "index": 0 }]
    ]
  }
}

The first inner array is output 0 (true), the second is output 1 (false). Fan-out to several nodes from one output means several entries in the same inner array:

"main": [[
  { "node": "Send Email", "type": "main", "index": 0 },
  { "node": "Log Result", "type": "main", "index": 0 }
]]

AI nodes use different connection types entirely, which is worth knowing when reading an agent workflow:

"connections": {
  "OpenAI Chat Model": {
    "ai_languageModel": [[{ "node": "AI Agent", "type": "ai_languageModel", "index": 0 }]]
  },
  "Calculator": {
    "ai_tool": [[{ "node": "AI Agent", "type": "ai_tool", "index": 0 }]]
  }
}

So main is the data flow, and ai_languageModel, ai_tool, ai_memory are the attachment points on an agent node. A tool connected as main instead of ai_tool is a common mistake in hand-edited agent workflows and produces confusing behaviour rather than a clear error.

Why an imported workflow fails to load

Almost always one of four things.

A node type your instance does not have. Community nodes must be installed first, and a workflow exported from a newer n8n may use nodes that did not exist in yours.

python3 -c "import json,sys; print(sorted({n['type'] for n in json.load(open(sys.argv[1]))['nodes']}))" workflow.json

That lists every node type the file requires, which turns a vague failure into a specific missing package.

A typeVersion newer than your node supports. Parameters are interpreted against the schema version, and a workflow built on a newer n8n can specify a version your node does not know.

A connection referencing a node name that does not exist. Usually from a rename that was not propagated, or a hand-edit. Check for it:

import json
wf = json.load(open('workflow.json'))
names = {n['name'] for n in wf['nodes']}
for source, outputs in wf['connections'].items():
    if source not in names:
        print(f'connection from missing node: {source}')
    for conn_type in outputs.values():
        for output in conn_type:
            for link in output:
                if link['node'] not in names:
                    print(f'{source} -> missing node: {link["node"]}')

Invalid JSON. Usually a truncated copy-paste, or a trailing comma. python3 -m json.tool workflow.json reports the exact position.

Keeping workflows in version control

This is the strongest reason to care about the JSON. Workflows are production logic, and logic that only exists inside a running instance has no history, no review and no rollback.

Export via the CLI rather than clicking through the UI:

n8n export:workflow --all --separate --output=./workflows
n8n export:workflow --id=42 --output=./workflows/report.json

# import
n8n import:workflow --separate --input=./workflows

--separate writes one file per workflow, which makes diffs readable and lets two people change different workflows without conflicting.

Two things to handle before committing:

Credentials. Exports reference credentials by ID and name. n8n export:credentials can include decrypted secrets with --decrypted, and that must never be committed. Export credentials separately, keep them out of the repository, and let each environment hold its own.

Noise in the diff. Node positions and id fields change without any logical change, which makes reviews harder. A normalisation step before committing helps:

jq -S '.' workflow.json > normalised.json

Sorting keys consistently means a diff reflects an actual change rather than a re-serialisation.

The practical result is a review process for automation logic — a pull request showing that someone changed the condition on the branch that decides whether an invoice is sent.

Editing in bulk

The canvas is fine for one node and poor for twenty. Bulk edits are where working with the JSON directly clearly wins.

import json, pathlib

for path in pathlib.Path('workflows').glob('*.json'):
    wf = json.loads(path.read_text())
    changed = False
    for node in wf['nodes']:
        params = node.get('parameters', {})
        url = params.get('url')
        if isinstance(url, str) and 'old-api.example.com' in url:
            params['url'] = url.replace('old-api.example.com', 'api.example.com')
            changed = True
    if changed:
        path.write_text(json.dumps(wf, indent=2))
        print(f'updated {path.name}')

Changing an API hostname across forty workflows is a two-minute script and an afternoon of clicking.

Rules that keep this safe:

  • Work on exported files in version control, never against the live database.
  • Validate the JSON after editing, before importing.
  • Import into a test instance first and run one execution.
  • Never hand-edit id fields — duplicates cause genuinely strange behaviour.
  • If you rename a node, update every reference in connections.

A useful validation pass before import: parse the JSON, check every connection target exists, and confirm every node type is installed. Those three checks catch nearly every broken import.

How this fits the rest of the stack

Treating workflows as code is what turns automation from something one person maintains in a browser into something a team can review, roll back and reproduce. That requires the workflows to be exportable, the instance to be reproducible, and the credentials to live outside both.

RunxBuild runs n8n as a managed tool with its own plan, environment variables, custom domains, autoscaling and logs, with a managed Postgres alongside for the execution database — so the instance your workflows import into is configured rather than assembled, and its state survives restarts. n8n on a $6 Basic plan with a Postgres beside it is a common starting shape. The RunxBuild hosting calculator shows what the tool and its database come to together.

Useful related references:

FAQ

What is the structure of an n8n workflow JSON file?

An object with a nodes array and a connections map. Each node carries type, typeVersion, name, position and parameters. Connections are keyed by source node name, then by connection type such as main, then an array per output index containing the target nodes.

Why does my imported n8n workflow fail to load?

Usually a node type your instance does not have installed, a typeVersion newer than your node supports, a connection referencing a renamed node, or malformed JSON. List the required types with a short script over the nodes array to turn a vague failure into a specific missing package.

How do I put n8n workflows in version control?

Export with n8n export:workflow --all --separate --output=./workflows, which writes one file per workflow so diffs stay readable. Keep credentials out of the repository entirely — exports reference them by ID, and decrypted exports must never be committed.

What do main, ai_tool and ai_languageModel mean in connections?

main is the normal data flow between nodes. ai_languageModel, ai_tool and ai_memory are attachment points on AI Agent nodes. Connecting a tool as main instead of ai_tool is a common hand-editing mistake and produces confusing behaviour rather than a clear error.

Can I edit n8n workflows outside the editor?

Yes, and it is much faster for bulk changes such as updating an API hostname across many workflows. Work on exported files in version control rather than the live database, validate the JSON, and import into a test instance before production. Do not hand-edit node id fields.

#n8n workflow json example#n8n#workflow automation#json#self-hosting