Module 1: Why Version Your Workflows

4. Anatomy of a workflow JSON

Description

By the end of this lesson you will be able to open an exported workflow's JSON file and name each of its main parts: the nodes, the connections, the configuration, the identifiers, and the credential references. And you will be able to make the distinction that matters most: separating stable fields —the ones that only change when you actually change the workflow's logic— from volatile fields —the ones that change for cosmetic or infrastructure reasons, without the workflow doing anything different. That distinction isn't a technicality: it's what decides whether your workflow's diff is readable or a wall of noise.

This matters because starting in Module 2 you're going to version this file, and versioning something you don't understand is versioning blind. When you see a diff of your workflow, you have to be able to look at the highlighted lines and say "this is a real logic change" or "this is just that I moved a node." Without that reading, every diff is going to look like chaos to you and you're going to lose the review capability that is half the value of versioning. This lesson gives you the eyes to read the file.

Connection to the module: lesson 3 closed by promising to open the JSON's "closed box," and this one opens it. It also directly prepares lesson 5: once you know what fields there are and which ones are volatile, lesson 5 shows you which of those volatile fields specifically break when you move the workflow to another instance —the credential IDs, the webhook ones. And it's essential for lesson 8's project, where you're going to read order-triage's JSON field by field. One methodological note up front: the exact field names and their presence can vary between n8n versions. I'm going to describe the general structure and ask you to confirm the details by opening your own exported file; don't trust that a field is word-for-word as shown here without checking it in your version.

The JSON is the workflow's blueprint

Before looking at the fields, let's fix what this file is, with an image.

When an architect designs a building, there are two different things: the built building —made of brick, with people inside— and the blueprint —sheets with lines describing where each wall goes, each door, each installation. The blueprint isn't the building; it's its complete description in a format that can be saved, copied, compared, and handed over. With the blueprint in hand, another builder could put up the same building on a different lot.

A workflow's JSON is the blueprint. The "built building" is your workflow running on the n8n instance: the nodes you dragged, the connections you drew, every field you filled in. The JSON is the complete description of all that in text form. When you do Download, n8n reads the building and hands you the blueprint.

And like any blueprint, it has a grammar: there are fixed sections, everything is in its place, and learning to read it is learning a language. The good news is that language is short. A workflow, however complex, is described with a handful of top-level sections, and everything else is a repetition of patterns inside those sections. Let's walk through them.

Let's recall what JSON is in one sentence, in case you're coming in without that context. JSON —JavaScript Object Notation— is a way of writing structured data. It uses braces { } to group a set of "name: value" pairs (an object), brackets [ ] for a list of things (an array), quotes for text, and : to separate each name from its value. An object can contain other objects and other lists, nested as deep as needed. You don't need to write JSON by hand for this lesson; just read it, and to read it, it's enough to recognize those four signals: braces, brackets, quotes, and colons.

The top-level structure

When you open an exported workflow, the first thing you see is one big object —the whole file is an object, wrapped in a pair of braces— with a handful of top-level keys. These are the ones you'll almost always find, with the warning that their exact name can vary by version:

KeyWhat it holdsIn one sentence
nameThe workflow's nameThe title you see in the editor
nodesThe list of all the nodesThe "bricks": each box on the canvas
connectionsHow the nodes link to each otherThe "arrows": what goes out from where and into where
settingsThe workflow's general configurationTimezone, error policy, execution saving
activeWhether the workflow is turned ontrue or false
pinData"Pinned" data for testingExample inputs frozen on certain nodes
meta / versionId / idMetadata and identifiersAdministrative data n8n uses internally
tagsThe workflow's tagsClassification for organizing

Of all of them, two hold almost all the substance —nodes and connections— and those are the ones we're going to look at closely. The others matter but are short.

nodes: the list of bricks

nodes is an array: a list where each element is a node from the canvas. If your workflow has seven boxes, this array has seven objects. Each object fully describes one node. Let's look at a real one from order-triage —the node that calls the CRM over HTTP— with the warning that I simplified it so it fits and is readable:

{
  "parameters": {
    "method": "GET",
    "url": "https://crm.example.com/api/customers/{{ $json.customer_id }}",
    "authentication": "genericCredentialType"
  },
  "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "name": "Get customer from CRM",
  "type": "n8n-nodes-base.httpRequest",
  "typeVersion": 4.2,
  "position": [1040, 300],
  "credentials": {
    "httpHeaderAuth": {
      "id": "27",
      "name": "Cumbre CRM - Header Auth"
    }
  }
}

Let's pause on each field, because these are the ones you'll see hundreds of times:

  • name — the name you gave the node in the editor, "Get customer from CRM." It's how you identify it visually and how other nodes reference it. It's a stable field: it only changes if you rename the node.

  • type — what kind of node it is, "n8n-nodes-base.httpRequest." It tells n8n what behavior to load. It's stable: it only changes if you replace the node with a different type.

  • typeVersion — the version of that node type, "4.2." n8n improves its nodes over time and keeps several versions so it doesn't break old workflows. It only changes when you update the node to a new version.

  • parameters — the node's configuration: this is where what you filled into the form lives. For this HTTP Request, the GET method, the URL it calls, the authentication type. This is the most stable and most important part: it's the node's actual logic. When you change a workflow's behavior, you're almost always changing something inside parameters.

  • position — where the node sits on the canvas, [1040, 300] (X, Y coordinates). This is the classic example of a volatile field: if you drag the node ten pixels to the right to tidy up the diagram, this number changes, and it shows up in the diff as if you'd modified the workflow, even though it does exactly the same thing.

  • id — a unique identifier for the node, that "a1b2c3d4-..." that looks like a barcode. n8n generates it on its own and uses it internally to refer to the node. It's volatile in a dangerous way: we study it in detail in lesson 5, because it can get regenerated on reimport.

  • credentials — the reference to the credential the node uses. This field is the heart of the portability problem, and deserves its own section.

The credentials field: a reference, not a secret

Look again at the credentials section of the node above:

"credentials": {
  "httpHeaderAuth": {
    "id": "27",
    "name": "Cumbre CRM - Header Auth"
  }
}

Here's an idea that's easy to misunderstand and that's central to the whole guide: the workflow's JSON does NOT contain your credential. It contains a reference to it.

Think of it as the key to a gym locker. Your CRM password or your API key isn't saved in the JSON; what's saved is something like "use credential number 27, called 'Cumbre CRM - Header Auth.'" The actual secret —the token, the password— lives encrypted in your n8n instance's database, somewhere else, not in this file. The workflow only says which credential to use, not what the credential is.

This is a good design decision by n8n, for security: it means you can share a workflow's JSON without giving away your secrets. But it has a huge consequence that's the topic of lesson 5: that "id": "27" only means something inside your instance. It's like the locker number: locker 27 at your gym isn't locker 27 at someone else's gym. When you take this workflow to another n8n instance, the "27" points to nothing —or worse, points to a different credential that happens to have that ID— and the node breaks. But let's not get ahead of ourselves; for now, hold on to the distinction: credentials stores a reference (an ID and a name), not the secret.

Notice too that two pieces of data show up: the id and the name. The id is what n8n actually uses to find the credential. The name is there for a human to read. And here's a security warning from lesson 3: that name sometimes reveals information —"Cumbre CRM - Production"— which is why the documentation recommends anonymizing credential names before sharing a JSON.

connections: the arrows between bricks

If nodes are the bricks, connections are the arrows you drew between them. This section describes which node feeds into which other node. In most n8n versions, connections are described by the node's name, not by its ID. A simplified fragment:

"connections": {
  "Get customer from CRM": {
    "main": [
      [
        { "node": "Classify order (AI Agent)", "type": "main", "index": 0 }
      ]
    ]
  }
}

Read it like this: "the main output (main) of the node named 'Get customer from CRM' connects to the input of the node named 'Classify order (AI Agent).'" It's the arrow, written as text.

An important detail you'll appreciate: because connections use the node's name and not its internal ID, they're relatively stable and readable. If you rename a node, you have to update both its name in nodes and every connection that mentions it —n8n does this for you in the editor. But as long as you don't rename anything, this section is one of the calmest in the file.

settings, active, and pinData: the rest

The other sections are shorter, but worth knowing.

settings holds the workflow's general configuration: the timezone it uses to interpret dates, what to do when a node fails, whether or not it saves the log of every execution. These are settings that affect the whole workflow, not one particular node. It's a stable field: it only changes when you change those settings.

active is a simple true or false: it says whether the workflow is turned on (listening for its trigger) or off. This field has a portability trap worth flagging ahead of time: when you import a workflow into another instance, n8n doesn't necessarily respect this value —it typically imports it deactivated, so it doesn't fire on its own before you've checked its credentials. It's sensible behavior, but it means active isn't something you can count on when moving a workflow.

pinData holds "pinned data": example inputs you freeze on certain nodes so you can test the workflow without actually running the earlier nodes. It's a testing tool —you'll use it a lot in Module 5— and for now it's enough to know that, if your workflow has pinned data, it travels inside the JSON. Watch out for this: pinned data can contain real information you captured during a test, so it's another spot worth checking before sharing a JSON.

order-triage in full, at a glance

Before moving on to the stable/volatile distinction, let's see how all these pieces assemble into a single workflow, so you stop seeing loose fields and see the whole blueprint. This is the very simplified skeleton of order-triage —I stripped out almost every parameter so it fits; in real life each node has much more inside it:

{
  "name": "order-triage",
  "nodes": [
    {
      "name": "Order received (Webhook)",
      "type": "n8n-nodes-base.webhook",
      "position": [600, 300],
      "id": "11111111-1111-1111-1111-111111111111",
      "webhookId": "f4b9c2a1-7d3e-4a8b-9c1d-2e5f6a7b8c9d",
      "parameters": { "path": "order-triage", "httpMethod": "POST" }
    },
    {
      "name": "Get customer from CRM",
      "type": "n8n-nodes-base.httpRequest",
      "position": [820, 300],
      "id": "22222222-2222-2222-2222-222222222222",
      "parameters": { "method": "GET", "url": "https://crm.example.com/api/customers/{{ $json.customer_id }}" },
      "credentials": { "httpHeaderAuth": { "id": "27", "name": "Cumbre CRM - Header Auth" } }
    },
    {
      "name": "Classify order (AI Agent)",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "position": [1040, 300],
      "id": "33333333-3333-3333-3333-333333333333",
      "parameters": { "promptType": "define" },
      "credentials": { "openAiApi": { "id": "14", "name": "Cumbre OpenAI - Dev" } }
    }
  ],
  "connections": {
    "Order received (Webhook)": {
      "main": [[{ "node": "Get customer from CRM", "type": "main", "index": 0 }]]
    },
    "Get customer from CRM": {
      "main": [[{ "node": "Classify order (AI Agent)", "type": "main", "index": 0 }]]
    }
  },
  "settings": { "timezone": "America/Mexico_City", "executionOrder": "v1" },
  "active": false,
  "pinData": {},
  "versionId": "b7e2d9-new",
  "meta": {},
  "tags": []
}

Read it top to bottom like a blueprint. At the top, name: the workflow is called order-triage. Then, nodes: three bricks —the webhook that receives the order, the HTTP call to the CRM, and the AI Agent node that classifies. Each with its name, its type, its position, its volatile id, and the two that use credentials with their reference ("id": "27" for the CRM, "id": "14" for the AI agent). Then, connections: the arrows, saying that from the webhook it goes to the CRM, and from the CRM to the AI Agent. And at the end, the configuration and metadata.

Notice three things that will matter in lesson 5. First: the webhook node has its own webhookId —another identifier that's generated per instance and is a source of breakage when moved. Second: the two credentials, "id": "27" and "id": "14", are numbers that only mean something in this instance. Third: the AI Agent points to a credential called "Cumbre OpenAI - Dev" —the Dev suffix is a hint that Cumbre has different credentials per environment, a topic for Module 4. With this complete blueprint in your head, the next lesson's breakage is going to make sense: it's not black magic, it's these concrete numbers pointing to the wrong place.

Stable versus volatile: the distinction that makes a diff readable

You already have the field map. Now the part that turns that map into a skill: separating stable from volatile.

A stable field is one that changes only when you change the workflow's logic. If it shows up in a diff, it's because you did something real: changed a URL, a threshold, a connection, a parameter. When you see a stable field modified, it's worth reading closely, because it means something.

A volatile field is one that changes without the workflow doing anything different. It shows up in the diff for cosmetic reasons —you moved a node— or infrastructure ones —n8n regenerated an ID. When you see a volatile field modified, it's noise: the behavior didn't change, only a detail changed that doesn't matter for what the workflow does.

Here's the table worth keeping in mind:

FieldStable or volatile?Why
parametersStableIt's the node's actual logic; changes only if you change behavior
name (of the node)StableChanges only if you rename it
type / typeVersionStableChanges only if you replace or update the node
connectionsStableChanges only if you rewire nodes (uses names, is readable)
settingsStableChanges only if you change workflow settings
positionVolatileChanges just from moving a node on the canvas
id (of the node)Volatile / dangerousCan be regenerated on reimport (lesson 5)
credentials.idVolatile / dangerousIt's a per-instance ID; breaks when moved (lesson 5)
versionId / metaVolatileAdministrative metadata n8n manages
activeNot reliable when movedNot reliably respected on import

Why this distinction is the center of the lesson: the goal of a clean diff is that the highlighted lines are almost all stable fields. If every time you save a version, the diff is full of changed position values and new versionIds, the noise buries the signal, and reviewing becomes impossible again —you're back to lesson 3's problem, but now inside the version system. That's why Module 3 devotes a whole lesson to normalizing the JSON: reordering the fields, and in some cases ignoring the volatile ones, so diffs show only what matters. You can't normalize what you can't tell apart; this lesson is the prerequisite.

Worked example: two diffs, one with signal and one with noise

Let's compare two versions of order-triage to feel the difference firsthand.

Diff A — a real logic change. You changed the CRM URL from an old server to a new one. The diff shows:

   "parameters": {
     "method": "GET",
-    "url": "https://old-crm.example.com/api/customers/{{ $json.customer_id }}",
+    "url": "https://crm.example.com/api/customers/{{ $json.customer_id }}",
     "authentication": "genericCredentialType"
   },

What to expect: a single changed line, inside parameters, a stable field. You read the diff and in two seconds you know exactly what happened and decide whether to approve it. This is pure signal.

Diff B — the same workflow, but you only moved three nodes and n8n regenerated metadata. The diff shows:

-  "position": [1040, 300],
+  "position": [1120, 340],
...
-  "position": [820, 300],
+  "position": [900, 280],
...
-  "versionId": "8f3a1c-old",
+  "versionId": "b7e2d9-new",

What to expect: several changed lines, all volatile. position three times —you moved nodes— and versionId once —n8n regenerated it. The workflow does exactly the same thing as before. None of these lines changed the behavior, but all of them clutter the diff. If this noise mixes with a real change, the real change gets lost inside it.

The skill you're building is looking at Diff B and saying, without hesitating, "this is all noise, there's no behavior change here," and looking at Diff A and saying "this is a real change, I'll review it carefully." That instant reading is what lets you version without drowning, and it's impossible without the field map you just learned.

Common mistakes

Believing the JSON contains the credentials (conceptual and security-related). What happens: someone assumes that because the node "has" the credential, the secret is in the JSON, and then either (a) is afraid to version the workflow, thinking they're uploading their passwords, or the opposite, (b) shares the JSON believing the credential travels along and will work on the other end. Both assumptions are false, and opposite of each other. Why it happens: the relationship between workflow and credential is indirect and not obvious until you open the file. How to spot it: open the JSON and look for the credentials section; you'll see an id and a name, never the secret itself. How to fix it: understand that the JSON stores a reference (ID and name), not the secret. That's why versioning it is safe with respect to the secret —though it's worth anonymizing the names— and that's why moving the workflow doesn't bring the credential with it, which causes the breakage covered in lesson 5.

Treating every diff change as equal (practical). What happens: someone starts versioning, sees a diff with fifteen changed lines, and either gets overwhelmed or —worse— reviews all fifteen with the same attention, wasting time on position changes that don't matter and sometimes missing the one parameters line that does. Why it happens: without the stable/volatile distinction, every highlighted line looks equally important. How to spot it: if reviewing a diff takes the same effort no matter how many real changes it has, you're not filtering the noise. How to fix it: train your eye to skip volatile fields (position, versionId, id, meta) and stop at the stable ones (parameters, connections, name). And attack the root cause in Module 3, normalizing the JSON so the volatile ones don't even show up in the diff.

Renaming nodes carelessly without seeing the effect on connections (practical). What happens: someone renames a node thinking it's a cosmetic change and is surprised to see the diff full of changes in the connections section. Why it happens: because connections are described by the node's name, renaming a node touches every connection that mentions it. How to spot it: if a "simple" rename produces a big diff, this is what happened. How to fix it: it's not a serious mistake —n8n keeps things consistent for you in the editor— but it's worth knowing so you don't get scared reading the diff, and so you understand why frequent renames clutter the history. Rename with intent, not out of habit.

Assuming the JSON structure is identical across every n8n version (conceptual). What happens: someone memorizes the field names from a tutorial and gets confused when their version has different ones or adds others. Why it happens: n8n evolves its format across major versions, and documentation from a year ago may not match. How to spot it: if a field that "should be there" doesn't show up, or there are fields you don't recognize, it's a version difference. How to fix it: learn the general structure —nodes, connections, configuration, IDs, credentials— which is stable over time, and confirm the exact names by opening your own exported file. The skill of reading the file transfers; verify the literal names in your version.

Exercises

Exercise 1 — Map your own workflow. Take the .json file you exported in lesson 3 (or export a new one). Open it with a text editor and locate, pointing to the line: (a) the nodes key, (b) the connections key, (c) the name and type of at least one node, (d) a position field, (e) if any node has credentials, its credentials section with the id and the name.

See solution

There's no single answer because it depends on your workflow, but there is a check. You should have found nodes as a list (starts with [), connections as an object mentioning node names, and inside each node the fields name, type, position, and parameters. If your workflow uses any node with authentication —HTTP Request, an app node, an AI Agent— the credentials section has an id (a number or short string) and a readable name; never the secret.

If you don't find credentials in any node, it's because your workflow doesn't use any credential —which is fine, it just means lesson 5's portability problem won't affect that particular workflow.

Why it works: reading your own file closes the gap between the abstract description and the real object. A field you located with your finger on your own file doesn't get forgotten. And you discovered, along the way, the warning that exact names can vary from this lesson's depending on your version, which was half the point of the exercise.

Exercise 2 — Classify eight changes. For each of these changes to order-triage, say whether the resulting diff would be mostly signal (stable fields, real behavior change) or mostly noise (volatile fields, no behavior change):

(a) Changing the manual-review threshold from 5000 to 3000. (b) Dragging every node around to tidy up the diagram so it looks neater. (c) Changing the CRM URL to a new server. (d) Adding a connection from one node to a new node. (e) Saving the workflow without changing anything (n8n regenerates versionId). (f) Renaming the "HTTP Request" node to "Get customer from CRM." (g) Changing the timezone in settings. (h) Importing the workflow into another instance, which regenerates the node ids.

See solution

(a) Signal. Changes a value inside parameters. Real behavior change.

(b) Noise. Only changes position on several nodes. The workflow does the same thing.

(c) Signal. Changes the url in parameters. The node now calls somewhere else: different behavior.

(d) Signal. Changes connections (and adds a node to nodes). The flow genuinely changes.

(e) Noise. Only changes versionId and maybe meta. Identical behavior.

(f) Mixed, with a nuance. Changes the node's name (stable) and every connections entry that mentions it. It doesn't change behavior, but it's not cosmetic noise like position either: it's an intentional name change. It looks big in a diff, but it's a conscious change, not regenerated infrastructure.

(g) Signal. Changes settings. Affects how the workflow interprets dates: real behavior.

(h) Dangerous noise. Changes the node ids, which are volatile. Behavior doesn't change, but —a preview of lesson 5— this kind of regeneration is exactly what causes problems when moving a workflow. It's noise in the diff, but it's not harmless.

Why it works: if you got most of these right, you can already tell signal from noise, which is this lesson's whole skill. Cases (f) and (h) are the interesting ones because they're neither pure signal nor pure noise: (f) is an intentional change that looks big, and (h) is noise that nonetheless matters. Those nuances are what separate a mechanical reading from a reading with judgment.

Exercise 3 — Explain the credentials field in your own words. Without rereading the section, write in three or four sentences exactly what a node's credentials field stores, why it does NOT store the secret, and what consequence that has for (a) versioning the workflow and (b) moving it to another instance. Use your own analogy if it helps.

See solution

One possible version:

"The credentials field stores a reference to the credential —an id and a name— not the credential itself. The actual secret (the token, the password) lives encrypted in the instance's database, not in the JSON. It's like a locker number: the JSON says 'use locker 27,' but the locker's key is somewhere else. For (a) versioning, this is good: I can save the workflow in Git without uploading my secrets (though it's worth anonymizing the name). For (b) moving to another instance, this is a problem: 'locker 27' on my instance isn't the same as locker 27 on the other one, so the reference points to nothing and the node breaks."

Why it works: if your explanation distinguishes reference from secret and draws out the two opposite consequences —good for versioning, problematic for moving— you understood the most important concept for lesson 5 and for all of Module 4's credential handling. The locker analogy (or any one that separates "the number" from "what's inside") is what makes it stick.

Summary and next step

In this lesson you opened the JSON box and saw that it's the workflow's blueprint: its complete description in text, with a short grammar. You walked through the top-level structure —name, nodes, connections, settings, active, pinData, identifiers— and looked closely at the two sections with substance: nodes, the list of bricks, where each node has its name, type, parameters, position, id, and credentials; and connections, the arrows, which are described by node name and are therefore readable. You understood the most important idea for the rest of the guide: the credentials field stores a reference (ID and name), not the secret, which lives encrypted in the instance's database. And you learned to separate stable fields —parameters, connections, name, settings, which change only with logic— from volatile ones —position, id, versionId, which change for cosmetic or infrastructure reasons— because that distinction is what makes a diff readable or a wall of noise.

Before moving on you should be able to: name the main sections of the JSON; explain what credentials stores and what it doesn't; and classify a change as signal or noise depending on whether it touches a stable or volatile field.

What's next is the direct consequence of all this. You already know credentials.id is an ID that only means something inside your instance, and you already suspect moving the workflow is going to break it. Lesson 5 makes that danger explicit and complete: it walks through every fragile point of a naive reimport between instances —the credential IDs, the node IDs, the webhook IDs and URLs, embedded environment variables— and shows you why "I exported it and imported it" fails silently, with no big error to warn you, until someone runs the workflow and something blows up. It's the lesson that turns your reading of the JSON into a concrete list of risks to control.

Resources