Module 2: Git from Scratch for Automators

4. Reading a workflow's diff

Description

By the end of this lesson you will be able to answer the question version control exists to answer: what exactly changed between two versions of my workflow? You'll know what a diff is and how to read its anatomy —the headers, the block markers, the removed lines and the added ones— you'll handle git diff in its three most useful forms, and you'll read a real change inside order-triage, identifying which node got touched and which parameter. And you're going to face an uncomfortable reality head-on: an n8n workflow's JSON, as it comes out of the platform, produces noisy diffs, and you're going to learn to find the real change in the middle of that noise.

This matters because reading a diff is the skill that turns Git from "a box where I save versions" into "a tool that lets me understand how my system evolved." A history you can't read is almost as useless as having no history. And in real work, reading diffs is what you do before approving a change, before merging a branch, before promoting something to production, and —on incident day— to find what broke it. It's, perhaps, the most transferable skill in the whole module.

Connection to the module: this is the hinge. Lessons 2 and 3 were mechanics —install, initialize, save— useful, but you might still think Git is a save button on steroids. This is where Git shows what it's really for. You're going to compare the two commits you already have and make a third change to compare too. What you learn here you're going to use in every lesson that follows: to review a branch before merging it (lesson 5), to understand what a coworker's change brings (lesson 6), and to identify which commit to revert (lesson 7). And the JSON noise you're about to discover today is exactly the problem Module 3 solves with normalization; here we name it and learn to live with it, but we don't solve it: that's this module's boundary.

What a diff is

Let's start with the word. Diff is short for difference. A diff is a comparison between two versions of a text file that shows you, line by line, what was removed and what was added to go from one to the other. It doesn't show you the whole file: it shows you only what changed, plus a few lines of surrounding context so you can locate where the change is.

Think of it like a word processor's "track changes" mode, the one where what you deleted shows up struck through and what you added shows up underlined in another color. When you hand a document to someone to review your edits, you don't just send them the new document —they'd have to read the whole thing and guess what you touched— you send them the document with the changes marked, so they see at a glance what's different. A Git diff is exactly that, but for any text file, and generated automatically by comparing two versions Git already has saved.

The key difference from a word processor's "track changes" is that Git works line by line. Its unit is the whole line: if you changed a single word on a line, Git shows you that entire line as "removed" and the entire new line as "added." It doesn't underline the loose word. This matters for understanding why n8n's JSON behaves the way it does, and we'll come back to it later.

A diff's anatomy, read slowly

Before generating one over order-triage, let's look at the general shape of a Git diff with a tiny example, so when the real one shows up it doesn't catch you off guard. Suppose any text file where you changed one line:

diff --git a/order-triage.json b/order-triage.json
index c08b4a2..3e5d720 100644
--- a/order-triage.json
+++ b/order-triage.json
@@ -12,7 +12,7 @@
         "promptType": "define",
         "text": "=Classify this order.",
-        "timeout": 5000,
+        "timeout": 15000,
         "options": {}

Each part has a job. Let's go top to bottom:

  • diff --git a/order-triage.json b/order-triage.json: the title. Says which file is being compared. The a/ is the old version and b/ is the new one; they're Git's conventional labels, not real folders.
  • index c08b4a2..3e5d720 100644: internal Git information —the two versions' IDs and their permissions. You can ignore it 99% of the time; it's there for Git's internals.
  • --- a/order-triage.json and +++ b/order-triage.json: define the two sides of the comparison. The three dashes --- mark the old version; the three plus signs +++ mark the new version. Memorize this pair: minus is old, plus is new. It's the key to reading everything that follows.
  • @@ -12,7 +12,7 @@: the block header —called a hunk in English. It tells you which part of the file this change is in. Read it like this: -12,7 means "in the old version, this chunk starts at line 12 and spans 7 lines"; +12,7 means the same for the new version. When a file has several changes in different places, you're going to see several of these @@ headers, one per touched area. They're your "the change is around here" markers.
  • The lines below are the change itself. Look at the first character of each one:
    • Ones starting with a space ( ) are context: lines that did not change, shown so you can locate where you are. Here, "promptType": "define",, "text": ..., and "options": {} weren't touched.
    • The one starting with - is a line that got removed (it was in the old version, it isn't anymore): "timeout": 5000,.
    • The one starting with + is a line that got added (it wasn't in the old version, now it is): "timeout": 15000,.

Read all together, this diff tells a one-sentence story: in this workflow, the timeout went from 5000 to 15000. One old line removed, one new line put in. When you change a word or a number inside a line, this is always how it looks in Git: the whole line comes out as removed and its new version comes in as added. It's not that you deleted and rewrote everything; it's how Git, working line by line, represents a change inside a line.

With this anatomy in your head, any diff becomes readable. It's always the same: a title, two sides (minus is old, plus is new), zone markers (@@), and context, removed, and added lines. Let's generate a real one.

The three forms of git diff you're going to use

git diff compares two things, but which two things depends on how you call it. There are three variants covering almost everything you need. The reason they exist is direct: remember from lesson 3 that a change travels through three zones —working tree, staging area, and history— and sometimes you want to compare one against another.

1. git diff (bare): what you changed but haven't staged yet. Compares your working tree against the staging area. That is, it shows you the changes you made and haven't yet put in the frame with git add. It's the question "what have I touched since the last time I staged?"

2. git diff --staged: what you've staged but haven't saved yet. Compares the staging area against the last commit. It shows you exactly what would go into your next commit if you made it right now. It's the review you do right before committing: "is what I'm about to save what I think it is?" (Also written git diff --cached; they're the same thing.)

3. git diff <old-commit> <new-commit>: what changed between two photos in the history. Compares any two commits, using their IDs. It's the question we answered in lesson 1 —"what changed between Tuesday's version and today's?"— and the most powerful of the three, because it lets you travel through your entire history comparing any pair of points.

There's a fourth one worth keeping handy: git diff HEAD compares your working tree against the last commit, regardless of what's staged and what isn't. It's the "what's changed in total since my last photo?"

You don't need to memorize them today. Over time your hand picks them on its own. For now hold on to the two most used: git diff for seeing what you just touched, and git diff <commit> <commit> for comparing two versions from the history.

Worked example: reading a real change in order-triage

Let's make a real change to the workflow and read it with git diff. The change: adding a category to the classifier. So far, order-triage's AI Agent node classifies orders as standard or priority; we're going to add wholesale, because Cumbre started distinguishing its wholesale customers.

Step 1 — Start from a clean tree. Confirm you don't have loose changes:

git status

It should say nothing to commit, working tree clean. Good: any diff we see now will be from the new change, with no noise from old changes.

Step 2 — Make the change in n8n. Open order-triage, go into the Classify Order node (the AI Agent), and in its instruction text, change the category list from standard, priority to standard, priority, wholesale. Save, export the workflow, and replace your order-triage.json.

Step 3 — Ask what changed, without staging yet.

git diff

What to expect (in the ideal world). If the JSON were clean, you'd see something as readable as this:

diff --git a/order-triage.json b/order-triage.json
index 3e5d720..a91c3f8 100644
--- a/order-triage.json
+++ b/order-triage.json
@@ -22,7 +22,7 @@
       "parameters": {
         "promptType": "define",
-        "text": "=Classify this order into one of: standard, priority.",
+        "text": "=Classify this order into one of: standard, priority, wholesale.",
         "options": {}
       },
       "name": "Classify Order",

Read it with the anatomy you already know. The header @@ -22,7 +22,7 @@ takes you to the change zone. The context line below, "name": "Classify Order", tells you which node got touched: the classifier. And the minus/plus pair tells you which parameter changed and how: the text instruction went from listing two categories to listing three. Without opening n8n, without remembering what you did, the diff tells you the whole thing: in the Classify Order node, the wholesale category was added to the classifier's instruction. That sentence is what you'd write as a note if you were about to commit —it's almost verbatim lesson 1's Add wholesale category to the order classifier.

Exiting the diff. Same as with git log, if the diff is long and fills the screen, you exit with the q key.

That "ideal world" is where we're headed. But let's be honest about what you're actually going to see today.

The uncomfortable reality: n8n's JSON produces noisy diffs

Here comes the part no market curriculum tells you about, and that's half the reason this guide exists. The clean diff above is what we'd want to see. What an n8n workflow produces out of the box is, almost always, messier. There are two versions of the problem, and it's worth knowing both.

Problem A: JSON on a single giant line

When you export a workflow certain ways —copying and pasting the nodes, or with certain export configurations— the JSON comes out minified: the whole workflow on one very long line, with no line breaks or indentation. It looks like a wall of text with thousands of characters run together.

Remember Git works line by line. If your entire workflow is one line, then any change —no matter how tiny— makes Git see "line 1 changed," and since it can't show you inside the line, it shows you the whole line as removed and the whole line as added. The diff looks like this:

@@ -1 +1 @@
-{"name":"order-triage","nodes":[{"parameters":{"httpMethod":"POST", ... (thousands of characters) ... }]}
+{"name":"order-triage","nodes":[{"parameters":{"httpMethod":"POST", ... (thousands of characters) ... }]}

Two almost identical walls of text, and somewhere in the difference between them is your one-word change. It's, for practical purposes, unreadable. This is the worst case, and it's completely real.

Problem B: multi-line JSON, but with noise

The good news is that the download option from n8n's editor normally produces formatted JSON —multi-line and indented— which is already much better to diff than the one-line wall. The bad news is that even so it comes with noise: changes that show up in the diff but don't reflect any real modification to your workflow's logic. The three most common culprits:

  • The version identifier. Every time you save, n8n usually updates an internal field like versionId. That value changes even if you haven't touched any node, so it shows up in all your diffs as one removed and one added line, meaning nothing to you.
  • Canvas positions. Every node saves its position, the coordinates where it sits on the canvas. If you dragged a node three pixels to tidy it up visually, that position changes and shows up in the diff —even though moving a node changes absolutely nothing about what the workflow does.
  • Pinned data (pinData). If you tested the workflow with pinned data —a technique you'll see in Module 5— that sample data gets saved in the JSON and clutters the diff with content that isn't workflow logic.

A real diff, then, doesn't look as clean as my ideal example. It looks more like this, with your real change buried in the noise:

@@ -3,7 +3,7 @@
   "name": "order-triage",
-  "versionId": "a1b2c3d4-1111-2222-3333-444455556666",
+  "versionId": "f9e8d7c6-9999-8888-7777-666655554444",
   "nodes": [
@@ -18,7 +18,7 @@
         "promptType": "define",
-        "text": "=Classify this order into one of: standard, priority.",
+        "text": "=Classify this order into one of: standard, priority, wholesale.",
         "options": {}
@@ -40,7 +40,7 @@
       "name": "Classify Order",
       "typeVersion": 1.7,
-      "position": [200, 320],
+      "position": [208, 336],
       "id": "7c3e1a9b-..."

There are three change zones (@@), but only one is real: the middle one, where wholesale got added. The first one is versionId changing on its own. The third one is that, without meaning to, you moved the Classify Order node a few pixels while editing it. Two out of three changes are noise.

The skill you can actually build today: reading through the noise

We're not going to solve the noise in this lesson —that's Module 3, with normalization. But you can learn to read through it, which is a valuable skill on its own. The technique is simple and comes down to knowing what to ignore:

  1. Skip the versionId, id, meta, and instanceId zones. They're platform metadata, not your logic. When an @@ only touches one of those fields, move past it.
  2. Skip the position zones. A change from [200, 320] to [208, 336] is a node that moved on the canvas. Never logic; always canvas cosmetics.
  3. Skip the pinData zones if they show up. They're test data, not configuration.
  4. Stop at the parameters. That's where the real logic lives: an HTTP Request's URL, an AI Agent's instruction text, an If condition, a Webhook's path. The change you care about is almost always inside a "parameters": { ... } block, and the nearby "name": "..." context line tells you which node it belongs to.

With those four rules, the noisy diff above reads in five seconds: you ignore the versionId (rule 1), you ignore the position (rule 2), and you're left with the middle zone, which touches a parameters and sits near "name": "Classify Order." That's your real change. The noise is still there; you just learned to look through it.

And here I'll announce the boundary, without crossing it: in Module 3 you're going to learn to eliminate that noise at the root, normalizing the JSON before saving it —always exporting it with consistent formatting, removing volatile fields like versionId and positions, and ordering the keys— so your diffs come out as clean as my "ideal world." It's repository configuration work that deserves its own space. For now, knowing how to read through the noise is exactly the right skill: it lets you work today, with what n8n produces out of the box, while the real solution arrives.

Comparing two commits from the history

The most useful form of git diff in the medium term is comparing two photos from the past. Now that you have several commits, you can do it.

First, look at your history to choose what to compare:

git log --oneline
a91c3f8 (HEAD -> main) Add wholesale category to the order classifier
3e5d720 Widen CRM lookup timeout to 15 seconds
c08b4a2 Add order-triage workflow

(This assumes you already saved the wholesale change with a commit; if you haven't, stage it and save it with git add order-triage.json and git commit -m "Add wholesale category to the order classifier".)

Suppose you want to see everything that changed between the workflow's first version and the current one. Take the first commit's ID and the last one's:

# git diff <old> <new>: what needs to change to go from old to new.
git diff c08b4a2 a91c3f8

What to expect. A diff bringing together all the changes between those two photos: the timeout that went from 5000 to 15000 (from the second commit) and the wholesale category that got added (from the third), plus whatever metadata noise piled up along the way. Order matters: git diff old new shows you how to get from the old to the new; if you flip them (git diff new old), you'd see the change backward —what got added would show up as removed. The mnemonic rule: the first ID is the starting point, the second is the destination.

A handy shortcut: if you want to compare a commit against your current version, you can use HEAD instead of typing the last one's ID. git diff c08b4a2 HEAD compares the first commit against where you're standing now. And to compare a commit against the one immediately before it, there's the a91c3f8~1 notation (the ~1 means "one before this one"), but that's a refinement; the direct IDs are enough for the whole module.

Common mistakes

Getting scared by the diff's size and believing you broke something (conceptual). What happens: you make a tiny change —raise a timeout, change a word— and git diff gives you back a screen full of red and green lines. The instinctive conclusion is "I changed way more than I thought" or "something broke." Why it happens: it's n8n JSON's noise —versionId, positions, maybe a key reordering on re-export— on top of Git showing the whole line even if you only changed one word. The diff's size isn't proportional to the real change's size. How to spot it: apply the four "reading through the noise" rules; if skipping metadata and positions leaves you with a single real change inside a parameters, the big diff was almost all noise. How to fix it: don't measure a change by its diff's size. Look for the parameters zones and evaluate those. And be patient: Module 3 slims these diffs down to their honest size.

Confusing git diff with git diff --staged and believing "there are no changes" (practical). What happens: you modify the file, run git add, then run git diff to review and see nothing; you conclude your change got lost. Why it happens: bare git diff compares the working tree against the staging area, and since you already staged everything with add, there's no difference left between those two zones —your change is in the staged one, not "further ahead." How to spot it: if git diff comes out empty but git status says there's something in "Changes to be committed," the change is staged. How to fix it: to see what you already staged, use git diff --staged. Practical rule: before the add, review with git diff; after the add and before the commit, review with git diff --staged.

Flipping the commit order when comparing (practical). What happens: you run git diff <new> <old> instead of <old> <new>, and the diff shows you everything backward —what you added shows up with - (removed) and what you removed with +— which completely confuses the reading. Why it happens: it's easy to forget which ID goes first. How to spot it: if a change you know was an addition shows up marked as removed, you flipped the order. How to fix it: remember the rule —git diff shows you how to go from the first to the second, so the starting point (the old one) goes first and the destination (the new one) goes second. Minus is always the one you named first; plus is always the one you named second.

Diffing a minified JSON and giving up (practical). What happens: your workflow is exported on a single line, git diff shows two visually identical walls of text, and you conclude Git doesn't work for workflows. Why it happens: Git works line by line and a one-line JSON stops it from showing the internal change. It's not a Git failure; it's the file's format. How to spot it: if your diff is always @@ -1 +1 @@ followed by two huge lines, your JSON is minified. How to fix it: for today, export the workflow with the download option from the editor, which produces multi-line formatted JSON that already diffs reasonably. The full fix —normalizing the JSON so it always comes out clean and ordered— is Module 3. Don't give up on Git; the problem is the file's format, and it's fixable.

Exercises

Exercise 1 — Read a diff by hand. Without running anything, read this order-triage diff and answer: (a) which node got touched?, (b) which parameter changed and from what value to what value?, (c) which of the three change zones, if there's more than one, is noise and which is real?

@@ -5,7 +5,7 @@
   "name": "order-triage",
-  "versionId": "11111111-aaaa-bbbb-cccc-222222222222",
+  "versionId": "99999999-dddd-eeee-ffff-333333333333",
   "nodes": [
@@ -60,7 +60,7 @@
       "parameters": {
         "method": "GET",
-        "url": "https://crm.cumbre.example/api/customers",
+        "url": "https://crm-staging.cumbre.example/api/customers",
         "options": {
       },
       "name": "Lookup Customer in CRM",
See solution

(a) The Lookup Customer in CRM node —the HTTP Request that queries the CRM. You know it from the context line "name": "Lookup Customer in CRM" right below the second change block.

(b) The url parameter changed from https://crm.cumbre.example/api/customers to https://crm-staging.cumbre.example/api/customers. In other words, the CRM query now points at the staging server instead of the production one.

(c) There are two change zones. The first (versionId) is noise: it's metadata n8n changes on its own when saving. The second (the url) is the real change, and it's also exactly the kind of change that caused Cumbre's "bad Tuesday" in lesson 1 —pointing the CRM at staging, which doesn't have the wholesale customers' data.

Why it works: this exercise is reading through the noise in pure form. You applied rule 1 (skip versionId) and rule 4 (stop at parameters) without anyone dictating them to you. And along the way you saw a diff that tells a story with consequences: reading this diff well, in real life, is what lets you diagnose an incident in thirty seconds.

Exercise 2 — Choose the right command. For each situation, say which form of git diff you'd use: git diff, git diff --staged, or git diff <commit> <commit>.

(a) You just edited order-triage.json and want to review what you touched before staging anything. (b) You already did git add and want to review exactly what's going into your next commit. (c) You want to know what changed between your workflow's version from three commits ago and now.

See solution

(a) Bare git diff. Compares your working tree against the staging area, so it shows you what you changed and haven't staged yet.

(b) git diff --staged. Compares the staging area against the last commit, which is precisely what would go into your next commit. It's the "last look before saving" review.

(c) git diff <old-commit> <new-commit>, using the IDs (for example git diff c08b4a2 HEAD, where HEAD is "now"). It's the only one of the three that travels through the history comparing two saved photos.

Why it works: the three forms map to lesson 3's three zones. (a) work against staging, (b) staging against history, (c) history against history. Once that correspondence is clear, choosing the command stops being memory and becomes deduction: "which two zones do I want to compare?"

Exercise 3 — Tell real from noise. You're given this order-triage diff with four change zones. Classify each one as "real logic change" or "platform noise," and for the real ones say which node and which parameter they touch.

@@ -4,7 +4,7 @@
-  "versionId": "aaaa-1111",
+  "versionId": "bbbb-2222",
@@ -30,7 +30,7 @@
       "parameters": {
-        "path": "order-triage",
+        "path": "order-intake",
       "name": "Receive Order",
@@ -52,7 +52,7 @@
      "typeVersion": 2,
-      "position": [-40, 320],
+      "position": [-40, 288],
       "name": "Receive Order",
@@ -78,7 +78,7 @@
       "parameters": {
         "options": {
-          "timeout": 5000
+          "timeout": 15000
         },
       "name": "Lookup Customer in CRM",
See solution
  • Zone 1 (versionId): noise. Platform metadata.
  • Zone 2 (path): real change. Touches the Receive Order node (the Webhook), and changes its path from order-triage to order-intake. Watch out: this is a change with consequences —the webhook's URL changed, so whoever sends orders to the old address is going to fail. A diff like this deserves a clear commit note.
  • Zone 3 (position): noise. The Receive Order node moved on the canvas (from 320 to 288 on the vertical axis); nothing about what it does changes.
  • Zone 4 (timeout): real change. Touches the Lookup Customer in CRM node and raises its timeout from 5000 to 15000 milliseconds.

Of four zones, two are noise and two are real. And the two real ones are very different in nature: the timeout one is a harmless tweak; the path one can break order intake. Reading the diff isn't just about separating real from noise, it's about calibrating each real change's risk.

Why it works: this is the lesson's central exercise in full form. In a real n8n diff, real and noise come mixed together, and your job is to filter them with the four rules and then judge the weight of what's left. That's, literally, the job of whoever reviews changes before approving them —what you're going to do with branches in lesson 5 and with team changes in lesson 6.

Summary and next step

In this lesson you learned to read a diff, which is the skill that turns Git from a box of versions into a tool for understanding your workflow's evolution. You saw what a diff is —a line-by-line comparison, like a word processor's track changes— and its anatomy: the title, the two sides (minus is old, plus is new), the @@ block headers marking the change zones, and the context, removed, and added lines. You handled the three forms of git diff: bare for what you changed without staging, --staged for what you're about to save, and between two IDs to compare any pair of photos from the history. And on order-triage you read a real change —a category added to the classifier— identifying the node by its name line and the parameter by the minus/plus pair.

Above all, you faced n8n JSON's uncomfortable reality: on a single line, the diff is unreadable; multi-line, it comes with noise from versionId, canvas positions, and pinData. And you learned to read through that noise with four rules —skip metadata, skip positions, skip pinned data, stop at the parameters— knowing Module 3 solves it at the root with normalization.

Before moving on to lesson 5 you should be able to: read any diff and say what got removed and what got added; choose between git diff, git diff --staged, and git diff <commit> <commit> depending on what you want to compare; and, faced with a noisy n8n diff, separate the real change from the metadata in under a minute.

So far, all your work has lived on a single timeline: main, one photo after another. That works while you make one change at a time and you're sure of it. But what happens when you want to try out an idea without risking the version that works? When you want to experiment with that wholesale category without the good order-triage becoming unavailable in case the experiment fails? That's what branches are for, and they're lesson 5's topic. You're going to learn to open a parallel line of work, change things on it with total calm, compare —with the diffs you just mastered— and, if it convinces you, merge it back into main. It's the technique that gives a team back the freedom to improve without fear.

Resources