Agentic Workflows with n8n
Not every agent needs a Python file. n8n is an open-source, self-hostable workflow tool where you wire nodes on a canvas — triggers, apps, an LLM, tools — into an agent that runs on a schedule or a webhook. This chapter builds one, and shows exactly where the drag-and-drop maps onto the loop you already coded by hand.
No-code tools let you build automations by dragging nodes (a trigger, an AI step, an action) and wiring them together, instead of writing a program. They're the fastest way to ship a useful AI workflow and to prototype an idea before coding it. This section covers the main platforms and when each fits.
The words you'll hear (in plain terms):
| Term | What it actually means |
|---|---|
| no-code / low-code | building software by configuration and visual wiring instead of writing code. |
| node | one step in a workflow — a trigger, an AI call, a database write. |
| trigger | the event that starts a workflow (a form submit, a schedule, a new email). |
| workflow | the connected chain of nodes from trigger to result. |
| webhook | a URL that lets one app poke another to start a workflow. |
What you need before starting:
- No programming needed — this is the one section you can do with zero code.
- A free account on the tool you want to try (n8n, Zapier, Make, or Flowise).
- Having read what an agent is helps but isn't required.
New to the topic? Read this box, then take the chapters in order — each section is tagged essential → expert so you always know the depth you're at.
Learning objectives
- Explain what n8n is and when a visual workflow beats hand-written code.
- Name the core building blocks: trigger, nodes, connections, expressions, credentials.
- Build an LLM-powered workflow, then upgrade it to a true AI Agent node with tools + memory.
- Map every canvas concept back to the agent loop from Chapter 4 / C2.
- Handle credentials, errors, and self-hosting safely.
What n8n is — and when to reach for it essential
Here's a scene you'll recognize the moment you work with a team. Someone asks for a small automation: "when a customer emails support, classify it and drop billing tickets into Slack." You could write a Python service — an API key in an env var, a webhook handler, a deploy, a place to run it, logs when it breaks. Three days later it works, and then the person who actually owns support wants to tweak the wording of the Slack message and can't, because it's code they can't read, on a server they can't touch. The automation becomes your permanent chore. That mismatch — a five-minute idea trapped behind an engineer — is the exact gap n8n was built to close.
The mental model is simplest if you picture a factory conveyor belt. A part drops on at one end (the trigger), moves past a line of stations that each do one small thing to it (the nodes), and comes off the end finished. You don't write the belt; you arrange the stations and draw the wires between them. That visual arrangement is the program — which is why a non-engineer can look at the canvas, see "email → classify → route to Slack," and change the last station without breaking the first. The workflow is legible because it's a picture, not a wall of syntax.
Now the precise definition, which will land because you have the picture: n8n is a workflow-automation platform where an automation is a directed graph of nodes, and you build it by dragging boxes onto a canvas and connecting them instead of writing code. Two properties make it worth a chapter of its own. First, it's self-hostable — you can run the whole thing on your own machine or server (the Docker lab below does exactly that), so your credentials and data never leave infrastructure you control. Second, it's fair-code, not "no-code-only": when the visual nodes run out, you can drop into a Code node and write real JavaScript or Python, so you never hit a hard ceiling. That combination is why it sits between click-only tools and hand-written services rather than competing head-on with either.
The common mistake is treating this as "the easy version for people who can't code." It isn't — the value is speed and ownership, not avoidance. A senior engineer reaches for n8n precisely because gluing five SaaS apps together with retries, a scheduler, and a shareable canvas is faster here than in Python, and hands it to a team that can maintain it. The skill this chapter teaches is knowing which side of the line a given problem falls on — and the table just below is your first cut at that judgement.
n8n ("nodemation") is a fair-code, self-hostable workflow-automation platform. You build automations by placing nodes on a canvas and connecting them; each node does one thing (fire on a schedule, call an API, run an LLM, branch on a condition) and passes data to the next. It sits between "click-only" tools like Zapier (N2) and writing everything in code: more power and control than the former, far less boilerplate than the latter.
Read it left-to-right along the top arrow: the further right you go, the more control you get — and the more work it costs you.
- The left box is click-only tools (Zapier, Make): quick to start but you hit a ceiling when the logic gets non-trivial.
- The right box is hand-written code (Python, the C2 path): full power, but you own the boilerplate — webhooks, deploys, retries, logging.
- The highlighted centre box is n8n: visual node wiring for speed, plus a Code node escape hatch so you never hit a hard ceiling.
- The green band underneath names the two properties that make n8n worth its own chapter — it is self-hostable (your keys stay on your infra) and fair-code (drop into real JS/Python when the nodes run out).
In short: n8n trades a little of code's raw power for a lot less boilerplate — and unlike click-only tools, it has no hard ceiling.
| Reach for n8n when… | Reach for code (C2) when… |
|---|---|
| Gluing many SaaS apps together (Gmail, Slack, Sheets, a CRM) | The logic is complex, custom, or performance-critical |
| The workflow should be visible & editable by a team | You need full testing, version control, and CI |
| You want a scheduler/webhook + retries for free | You're building a product, not an internal automation |
| You want to self-host and own your data | Fine-grained control of the agent loop matters (safety gates) |
The building blocks essential
Before you build anything, you have to be able to name the parts — and the good news is there are only five, and you already met their ideas in the agent chapters. The reason to learn the vocabulary first is practical: when a workflow misbehaves, the fix is almost always "the wrong kind of block, or a block wired to the wrong upstream data." If you can't tell a trigger from a node from a connection, you can't debug it. So spend two minutes here and the rest of the chapter reads itself.
Think of the whole thing as a sentence with grammar. The trigger is the subject that starts everything ("when an email arrives…"); the nodes are the verbs, each doing one action ("…classify it, then post it"); the connections are the word order that says which verb runs next; and the items flowing along the connections are the thing being acted on, carried forward station to station. A workflow reads left to right like a sentence, and every valid workflow has exactly one subject — one active trigger — because something has to decide when the sentence gets spoken.
Here is the one detail that trips up every beginner, so internalize it now: data does not flow as a single value — it flows as a list of items. Each item is a small bundle of JSON like { "message": "I was charged twice" }, and n8n runs each node once per item automatically. That's why a workflow that classifies one email works unchanged on a batch of fifty — the node body describes what to do to one item, and n8n loops for you. Beginners who assume "one run = one value" get baffled the first time a node emits three items and the next node runs three times; once you know items are a list, that behaviour is obvious instead of spooky.
The remaining two blocks are the plumbing that makes the sentence real. An expression — the {{ $json.field }} syntax — is how one node reaches back and reads a field a previous node produced, so stations can pass specifics forward, not just vague data. A credential is a stored, encrypted secret (an API key, an OAuth token) that a node references by name rather than containing — a separation that matters enormously the day you export or share a workflow, because the secret stays behind. Keep these five names in view as you read the diagram and table below; everything else in n8n is just which node you drop into this same skeleton.
This is the entire mental model of n8n in one picture: a workflow is just boxes (nodes) wired left-to-right, and data flows along the wires. Nothing here is code — you build this by dragging boxes onto a canvas and drawing the connections.
- Read it left to right, following the arrows. The leftmost box is the Trigger — the event that starts the workflow (the small label underneath says schedule / webhook, i.e. a timer or an incoming web request).
- Each box after it is a node that does one job: the LLM / Agent node calls a language model, the IF / branch node makes a yes/no decision, and the final App node takes an action in another tool (Slack / Sheets…).
- The arrows are connections — the wires you draw. The caption at the top says data travels along them as JSON "items": small bundles of named fields (like
{ "message": "..." }) that each node reads, changes, and passes on. - That's the whole system: trigger → node → node → action. Everything else in n8n is just which box you drop in and how you configure it.
In short: A node is a step, a connection is an arrow, and the JSON "items" are the data riding along the arrows. If you can read this graph, you can read any n8n workflow.
| Concept | What it is |
|---|---|
| Trigger node | Starts the workflow — a schedule, an incoming webhook, a new email, a chat message. Every workflow has exactly one active start. |
| Node | One unit of work: call an API, run an LLM, transform data, branch. 400+ app integrations ship built-in. |
| Connection | The wire between nodes. Data flows along it as a list of JSON items. |
| Expression | Inline templating to pull data from earlier nodes: {{ $json.email }}, {{ $node["Webhook"].json.body }}. |
| Credential | A stored, encrypted secret (API key, OAuth token) referenced by nodes — never pasted into the node itself. |
Lab N1.1 · Get n8n running essential
The fastest way to lose an afternoon with a new tool is the install. You find three tutorials, one wants a global npm package, another a cloud signup, a third a from-source build, and each leaves a slightly different mess on your machine. n8n sidesteps all of that with a single Docker command — and it's worth understanding why that command is the recommended path, not just copy-pasting it, because the reasoning is the same one that makes n8n worth self-hosting in the first place.
Here's the key idea in plain terms: self-hosting means the software runs on infrastructure you own, so your secrets and data stay with you. An n8n workflow holds live API keys and can read your email, write to your CRM, and post to your Slack. When you run it yourself, none of that ever passes through someone else's server. The trade is that you become the operator — you're responsible for keeping it running, patched, and locked down (which is exactly what the danger callout below hammers home). n8n Cloud exists for people who'd rather pay to skip that responsibility; the Docker path below is for people who want the control.
The lab uses Docker because a container is a pre-packaged, isolated copy of an app plus everything it needs to run, so "install n8n" collapses into "download and run this one image" — no version conflicts with whatever else is on your machine. The one subtlety that beginners miss, and that the walkthrough spells out, is the split between the container (disposable — the --rm flag throws it away when you stop it) and the volume (persistent — the -v n8n_data flag is a named box on your disk where your workflows and credentials actually live). Get that distinction and you'll never panic when the container disappears; forget it and you'll think you lost all your work on the first restart. Follow the three steps in order: run the container, add your Anthropic credential once so every later node can reference it by name, and drop a Manual Trigger so you can click-to-run while you build.
--rm flag deletes it on stop); the named volume persists on disk, so your workflows and credentials survive a restart.
Read it as two boxes side by side: the left is the throwaway part, the right is the part that keeps your work.
- The left box is the n8n container — the running process on port 5678. The
--rmflag means it is deleted when you stop it (the red strip: "gone after Ctrl-C"). - The right box is the volume (
-v n8n_data) — a named box on your disk where your workflows and credentials actually live. The green strip says it survives restarts. - The arrow between them is the container writing your data into the volume, so the data outlives the process that created it.
- The payoff line at the bottom is the "aha": re-run the exact same command and the volume re-attaches — which is why your account and workflows are still there after a restart.
In short: The container is disposable; the volume is your save file. Lose the container and you lose nothing; lose the volume and you lose everything.
- Run it locally with Docker (self-host is the whole point):
shell
docker run -it --rm --name n8n -p 5678:5678 \ -v n8n_data:/home/node/.n8n docker.n8n.io/n8nio/n8nOpen
http://localhost:5678and create the owner account. (n8n Cloud is the hosted alternative if you'd rather not self-host.) - Add your Anthropic credential. In Credentials → New, add an Anthropic API key once. Every LLM node references it by name — the key lives encrypted in n8n, not in a workflow.
- Create a blank workflow and drop a Manual Trigger so you can run it on demand while building.
This one command downloads n8n and starts it on your own machine, so you get the full visual editor in your web browser with nothing to install by hand. docker is a tool that runs pre-packaged apps in isolated "containers"; here it runs the official n8n package. The backslashes (\) at the line ends just mean "this command continues on the next line" — it's really one command.
docker runstarts a container.-itkeeps it attached to your terminal so you can watch its logs, and--rmauto-deletes the container when you stop it (your data is kept separately — see below).--name n8ngives the container a friendly name so you can refer to it later.-p 5678:5678is port mapping: it connects port 5678 inside the container to port 5678 on your computer. That's why the next step openshttp://localhost:5678—localhostmeans "this machine".-v n8n_data:/home/node/.n8nis a volume: it stores n8n's data (your workflows, credentials) in a named box on your disk that survives even though--rmthrows the container away. Without this, you'd lose your work on restart.- The last part,
docker.n8n.io/n8nio/n8n, is the image — the address of the n8n package to download and run.
What the output means: Your terminal prints startup logs and an "Editor is now accessible" line. Opening http://localhost:5678 in a browser shows the n8n setup screen where you create the owner account — you're now running n8n locally.
Try this: Stop it with Ctrl-C, then run the exact same command again. Because of the -v n8n_data volume, your account and workflows are still there — proof the volume, not the container, holds your data.
Lab N1.2 · An LLM in a workflow intermediate
Everyone's first instinct with an LLM node is to type a prompt straight into it and hit run — and it works, once. Then you realize the whole point of a workflow is that the input isn't fixed: a real email, a form submission, a Slack message arrives, and the model has to see that text, not the sentence you hardcoded while testing. The moment you feel that gap — "how does the incoming data actually reach the prompt?" — you've hit the one concept that makes n8n's LLM nodes different from pasting into a chat window. This lab exists to make that wiring concrete on the smallest possible example: a one-node classifier.
The bridge is the expression, and the cleanest way to understand it is as a mail-merge slot. When you write a form letter, you don't retype the name for every recipient — you write "Dear {{first_name}}," and the system fills the slot from each record. n8n's {{ $json.message }} is exactly that: a slot in your prompt that n8n replaces, just before sending, with the message field of the item flowing in. So the prompt template stays fixed while the data behind it changes on every run. $json means "the item arriving at this node" and .message reaches into its named field — the visual, canvas-side twin of writing item["message"] in Python.
Two working habits in this lab are worth naming because they're the same discipline you learned coding, just relocated to the canvas. Pinning sample data on the trigger freezes one known input ({"message": "I was charged twice!"}) so you can re-run the workflow repeatedly and get a comparable result each time — it's a fixed test case, the visual version of the classifier tests from Chapter 2, and it's how you tune a prompt without a live event firing every time. And clicking a node to inspect its exact JSON in and out is n8n's genuinely killer feature: instead of guessing why a downstream step misbehaves, you see the precise data that entered and left every station. That per-node visibility is the thing seasoned n8n users lean on hardest; get in the habit early. Now build the two-node chain in the lab and watch a support message turn into a single clean label.
Start simple: a workflow that classifies an incoming message. This is the visual twin of Chapter 2's classifier.
{{ $json.message }} is a slot n8n fills from the incoming item right before the call — so the same node handles every different message.
Follow the arrows left-to-right: two things on the left merge in the middle, and the model turns the merged text into one clean label on the right.
- The top-left box is your fixed prompt template — the instruction plus a slot written
{{ $json.message }}. It never changes between runs. - The bottom-left box is the incoming item: the JSON bundle flowing into the node, e.g.
{"message":"…twice!"}. This is the part that does change on every run. - The two arrows meet at the centre box — the actual prompt sent. n8n has replaced the slot with the item's
messagefield, so the model sees real text, not the placeholder. - The green box is the payoff: because you told the model to reply with only a label, it returns a single clean word (
billing) a later node can branch on.
In short: {{ $json.message }} pulls a field off the incoming item into a fixed template — the canvas twin of writing item["message"] in Python.
- Manual Trigger → Anthropic Chat Model node. Set the model to
claude-opus-4-8and the prompt:node: prompt
Classify this support message as bug | feature | billing | other. Reply with only the label. Message: {{ $json.message }}The
{{ }}expression pullsmessagefrom the trigger's data. - Pin sample data on the trigger (
{"message": "I was charged twice!"}) so you can run repeatedly with the same input — the visual version of a fixed test case (Chapter 2). - Execute the workflow. Click a node to inspect the exact JSON in and out — n8n's killer feature for debugging.
This is the text you type into the LLM node's prompt field — the instruction sent to Claude every time the workflow runs. It does the same job as Chapter 2's classifier, but you configure it in a box on the canvas instead of in Python. The header node: prompt is just a label telling you where this text lives.
- The first two lines are the instruction: tell the model exactly what to do (classify as bug | feature | billing | other) and exactly how to answer (reply with only the label). Being this specific is what keeps the output clean enough for a later node to act on.
- The blank line then
Message:gives the model the actual text to classify. Keeping the instruction and the data visually separate helps the model not confuse the two. {{ $json.message }}is an n8n expression — the{{ }}marks a slot that n8n fills in before sending.$jsonmeans "the JSON item coming into this node" and.messagepulls itsmessagefield. So if the trigger's item is{"message": "I was charged twice!"}, the model actually receives Message: I was charged twice!.
What the output means: The node outputs a single word — for the charged-twice example, billing — because you told it to reply with only the label. Click the node in n8n to see the exact JSON that went in and came out.
Try this: Change the pinned sample data to {"message": "the app keeps crashing"} and re-run. The {{ $json.message }} slot picks up the new text and the label should switch to bug — no other change needed.
Lab N1.3 · From workflow to AI Agent intermediate
You'll hit the wall the moment you ask your classifier a question it can't answer from the prompt alone. "What's today's EUR/USD rate?" A plain Chat Model node will happily invent a number — confidently, plausibly, wrongly — because all it can do is answer from what it already knows. The fix isn't a better prompt; it's a fundamentally different node that can go get the rate before answering. That leap — from a model that talks to a model that acts — is the single most important idea in this chapter, and in n8n it's a one-box upgrade: swap the Chat Model node for an AI Agent node.
If you built the agent in Chapter 4 by hand, you already know the machine hiding inside that box; if you didn't, here's the whole thing in one loop. An agent runs a cycle: reason about the goal, decide it needs a tool, call the tool, observe the result, then reason again — repeating until the task is done. In code (C2) you wrote that while loop yourself: send the messages, check whether the model asked for a tool, run it, append the result, resend. The AI Agent node is that loop, packaged. You no longer write the round-trips; you supply the three ingredients and the node runs the cycle internally. That's the exact same mental model you already own, with the plumbing hidden — which is why "if you understand Chapter 4, you understand this node" is not a slogan but a literal fact.
The three ingredients you attach are worth understanding as roles, because getting one wrong is the usual reason a canvas agent underperforms. The Chat Model is the brain — the actual Claude model doing the reasoning and choosing what to do next. Memory (the Window Buffer Memory node) is the notebook — it carries the conversation across turns, so a chat agent remembers what was said three messages ago instead of starting cold each time; it's the same messages list you appended to and resent by hand. And tools are the toolbox — sub-nodes the agent may call, each with a description that the model reads to decide when to reach for it. That last point is the one beginners underestimate: the agent selects tools purely from their descriptions, so a vague description ("does stuff with orders") makes it pick wrong, while a sharp one ("look up an order's status by its numeric id") makes it pick right. Read the diagram as manager-plus-resources, then build the agent in the lab and — crucially — ask it something that forces a tool call, so you can watch the loop actually fire.
A single LLM node answers. The AI Agent node acts — it runs the tool-use loop for you: given tools and a goal, it decides which tools to call, calls them, reads results, and loops until done. This is Chapter 4's agent, as one node.
This diagram shows how the single AI Agent node is assembled from smaller pieces you plug into it. Where a plain LLM node just answers, the Agent node acts: it can call tools, read the results, and keep going until the task is done — all handled inside the one node.
- The top box, "AI Agent node", is the orchestrator. The three arrows dropping down from it show the sub-parts you attach underneath it on the canvas.
- Chat Model (bottom-left) is the brain — the actual Claude model that does the thinking and decides what to do next.
- Memory (middle) stores the conversation so far, so a chat agent remembers what was said on earlier turns instead of starting fresh each message.
- Tools (HTTP, Sheets…) (bottom-right) are the actions the agent is allowed to take — call an API, read a spreadsheet, etc. The agent chooses which to use based on each tool's description.
- The caption underneath — the Agent node runs the reason → call tool → observe loop internally — is the key point: you supply the pieces, and the node does the repeated back-and-forth (the "agent loop") for you.
In short: Think of the Agent node as a manager and the boxes below as its resources: a brain (model), a notebook (memory), and a toolbox (tools). You hand it those three things; it figures out the rest.
- Drop an AI Agent node. Attach the Anthropic Chat Model as its model.
- Give it a tool. Attach an HTTP Request tool (e.g. a weather API) or a Google Sheets tool. Write a clear tool description — the agent picks tools by description, exactly like C2.
- Add memory (a Window Buffer Memory node) so a chat-triggered agent remembers the conversation across turns.
- Trigger it with a Chat Trigger and talk to it. Ask something that requires the tool and watch the node call it, read the result, and answer.
messages history you resent by hand in C2. The AI Agent node hides the loop plumbing — but it's the identical mental model. If you understand Chapter 4, you understand this node.The canvas ↔ code Rosetta Stone intermediate
There's a quiet anxiety that creeps in when you switch tools: the fear that you're learning something disposable. "If I get good at dragging n8n boxes, does any of it transfer the day I have to write the real thing in Python?" This section exists to kill that fear outright, because the honest answer is: a visual workflow is a program, and every box maps to a line of code you already know how to write. The canvas isn't a different world — it's the same computation wearing a different coat. Seeing the correspondence directly is what turns n8n from a black box into a tool you trust.
The table below is a literal dictionary between the two. A trigger node is your script's entry point — the event or handler that starts main(). A Chat Model node is one client.messages.create(...) call. A tool sub-node with a description is one entry in the tools=[...] array you pass to the API. The whole AI Agent node is the while tool-use loop you hand-wrote in C2. A Memory node is the messages list you keep appending to and resending. An expression {{ $json.x }} is just reading a field off the previous step's result. And a credential is a key loaded from an environment variable or secrets manager. Nothing on the left has no equivalent on the right; that's the point.
Read this two ways, because it pays off in both directions. Left-to-right, it lets you prototype on the canvas and know exactly what you'd write if you later needed the control of code — the visual version is a runnable spec. Right-to-left, it lets an experienced coder read any n8n workflow at a glance, because they can silently translate each box into the code they'd have written. That fluency — moving between canvas and code without friction — is precisely the FDE judgement this whole no-code module is building toward: knowing not just how to build each way, but which way a given problem deserves. Exercises 4 through 6 make you exercise that translation until it's automatic.
Read it row by row, left to right: each blue box on the left is an n8n node, and the arrow points to its exact code equivalent on the right.
- The left column is the canvas — the boxes you drag onto the n8n workspace (trigger, Chat Model, AI Agent, tool, expression).
- The right column is the code you'd write by hand in C2 / Chapter 4 — the entry point, a
messages.create(...)call, thewhileloop, atools=[...]entry, a field read. - The arrows are the dictionary: each is a one-to-one translation. The two purple boxes (AI Agent node ↔ the whole
whiletool-use loop) are the biggest single mapping — one box replaces the entire hand-written loop.
In short: Nothing on the canvas is a dead end — every box is a line of code you can write yourself the day you need the control.
| n8n concept | Code equivalent (C2 / Ch 4) |
|---|---|
| Trigger node | The entry point / event that starts your script |
| Chat Model node | client.messages.create(model=...) |
| Tool sub-node + description | A tool definition in the tools=[...] array |
| AI Agent node | The whole while tool-use loop |
| Memory node | The messages list you append to and resend |
Expression {{ $json.x }} | Reading a field off a previous step's result |
| Credential | Key loaded from env / secrets manager |
Common pitfalls advanced
The dangerous thing about a tool that ships a working demo in ten minutes is that it makes the hard parts feel optional. They aren't — they're just deferred. Every failure in the table below is one that doesn't show up on your laptop and only surfaces when the workflow meets real traffic, real secrets, or a public URL. The pattern worth internalizing is that "no-code" removed the boilerplate, not the responsibilities: your n8n instance still holds live API keys and still takes real actions in real systems, so it inherits every operational concern a hand-written service would have.
Read the pitfalls as three families. The first is secrets handling: pasting an API key into a node field bakes it into the workflow, so it leaks the instant you export, share, or version that workflow — which is the entire reason credentials exist as a separate, encrypted, reference-by-name mechanism. The second is exposure: an editor or webhook reachable from the open internet with no auth is a standing invitation, since anyone who finds the URL can drive your workflows and the credentials behind them; the fix is auth, HTTPS, and restricted webhook paths, exactly as you'd protect any secret-holding service. The third is trusting the model too much: routing a branch on free-text LLM output breaks the day the model phrases the label differently, and a vague tool description makes an agent pick the wrong tool — both fixed by forcing structure (a typed field, a sharp description) so downstream logic reads data, not prose.
The last row is the philosophy that ties them together, and it's the one to carry out of this chapter: "no-code" is not "no-ops." The workflow needs error handling on flaky nodes (retries plus an error branch), monitoring so you notice when it silently stops, and the same security posture you'd give a production API. Treating a canvas as somehow exempt from operations is how a convenient internal automation quietly becomes an unmonitored liability holding live credentials. Skim the fixes below now; you'll apply every one of them in the reliability and industry exercises on the practice ladder.
Read it row by row: each red box on the left is a way the workflow fails, and the arrow points to the green box that fixes it.
- The three numbered rows group every pitfall into three families: secrets handling, public exposure, and over-trusting the model.
- The red boxes (left) are the mistakes that look fine on your laptop and only bite under real traffic — a pasted key, an open webhook, a branch reading raw model prose.
- The green boxes (right), reached by each arrow, are the fixes: reference secrets as named Credentials, put the instance behind auth and HTTPS, and force structure (typed fields, sharp tool descriptions) so downstream logic reads data, not text.
In short: Sort any n8n failure into secrets, exposure, or over-trust — then apply that row's fix, which is the same discipline you'd give any production service.
| Pitfall | Fix |
|---|---|
| Pasting API keys into a node field | Use a stored Credential — never inline secrets |
| Exposing the editor / webhook publicly with no auth | Put n8n behind auth + HTTPS; restrict webhook URLs |
| Passing free-text LLM output into a branch | Use a structured-output parser so fields are typed |
| Vague tool descriptions | Describe when to use each tool — the agent selects by description |
| No error handling on flaky nodes | Add an Error Trigger workflow / node retry settings |
| Treating "no-code" as "no-ops" | It still holds secrets & runs live actions — secure & monitor it |
Exercises advanced
Exercise N1.1 — Ticket router
Context: The cleanest way to branch on an LLM's judgement is to make it emit a typed field, then let a deterministic node route on that field rather than on free text.
Your task: Build a Webhook Trigger → Chat Model (classify) → IF workflow that sends billing messages to a Slack channel and everything else to a Google Sheet.
Requirements:
- The Chat Model emits a structured object like
{"category":"...","summary":"..."} - A structured-output parser types the
categoryfield so the branch is not reading free text - The
IFnode compares{{ $json.category }}to"billing" - Two output branches wire to two different app nodes (Slack vs Sheet)
💡 Hint: Parse the model output into a typed field first; branching on parsed JSON is reliable in a way branching on raw text never is.
Show hint
Have the LLM node emit {"category": "...", "summary": "..."}. In the IF node compare {{ $json.category }} to "billing". Two output branches, two app nodes.
Exercise N1.2 — Tool-using agent
Context: An agent that quotes a number without calling a tool is hallucinating. The execution view is where you prove a tool actually fired.
Your task: Give an AI Agent node an HTTP Request tool that hits a public API (e.g. currency rates) and ask it a question that requires the tool.
Requirements:
- The agent has exactly one tool: an HTTP Request against a real public endpoint
- The question can only be answered correctly by calling that tool
- Confirm from the execution view that the tool node actually ran
- The returned number matches the API, not a plausible invention
💡 Hint: Trust the execution trace, not the answer text — a right-looking number with no tool call in the trace is a hallucination.
Exercise N1.3 — Map it back
Context: The point of the whole no-code module is seeing that a canvas agent and a hand-written tool loop are the same machine. Placing them side by side makes the equivalence concrete.
Your task: Take your Exercise N1.2 agent and write the equivalent Python using the C2 manual tool loop — a model, one tool definition, and the while loop.
Requirements:
- One tool definition mirrors the HTTP tool the n8n agent used
- A
whileloop drives model call → tool call → feed result back until the model stops - The code path and the canvas path answer the same question the same way
- Note which canvas piece maps to which line of the loop
💡 Hint: The canvas hides the while loop the Agent node runs for you — your job is just to make that loop visible in code.
🪜 Practice ladder beginner → industry
Six graded exercises, easy to real-world. Try each before opening its solution.
Context: Every n8n workflow is the same shape: a trigger fires, nodes transform an array of items, and connections wire them together. Before you build anything, you have to be able to name those parts.
Your task: Sketch the node graph for “when a webhook fires, call an LLM, post the result to Slack” and label each block as a trigger, an action node, or a connection.
Requirements:
- Exactly one trigger (
Webhook) that emits a single item from the request body - A linear chain: trigger → LLM node → Slack node
- Each connection passes an array of items (
{json:{...}}) downstream - State what each node reads off the item and what it writes back
💡 Hint: The whole workflow is one trigger plus nodes wired to pass an item array forward — name the role of each of the three blocks, don't add any.
Show solution
The linear three-node graph the lesson starts from:
[ Webhook Trigger ] --items--> [ OpenAI / LLM node ] --items--> [ Slack node ]
trigger action (transform) action (output)
Roles:
trigger Webhook starts the run, emits 1 item with the request body
node (LLM) LLM Chat reads item.json.text, writes item.json.reply
node (out) Slack posts item.json.reply to a channel
connection the wires pass ITEMS (an array of {json: {...}}) downstream
Every n8n workflow is this shape: one trigger, then nodes wired to pass an array of items forward.
Context: An LLM node earns its place only once its prompt is wired to real incoming data. n8n does that mapping with {{ $json.field }} expressions rather than code.
Your task: Configure the LLM node's parameters as JSON: a system prompt, a user field mapped from the incoming item, and a defined place for the output to land.
Requirements:
- System prompt is a fixed role instruction
- The user
promptpulls fields from the item via{{ $json.subject }}style expressions - The value is prefixed with
=so n8n evaluates it as an expression rather than sending it literally - A low
temperaturekeeps triage output stable - Name the downstream field the model output is read from
💡 Hint: The = prefix is the load-bearing detail — without it the {{ }} tokens are passed as plain text instead of being resolved.
Show solution
n8n maps upstream data with {{ $json.field }} expressions. The node config:
{
"node": "OpenAI Chat Model",
"parameters": {
"system": "You are a support triage assistant. Reply in one sentence.",
"prompt": "={{ $json.subject }}\n\n{{ $json.body }}",
"temperature": 0.2
}
}
Downstream nodes then read the model output. A common correctness detail: the = prefix marks the whole string as an expression, so {{ }} is evaluated rather than sent literally.
The mapping {{ $json.subject }} pulls the field from the current item — this is the "canvas" equivalent of item["subject"] in code.
Context: The jump from an LLM node to an AI Agent node is the jump from one fixed call to a model that chooses tools in a loop. The agent selects tools purely from their descriptions.
Your task: Upgrade the LLM node to an AI Agent node that can call an HTTP “lookup order” tool and a calculator; draw the agent graph and write the tool declarations.
Requirements:
- The Agent node is fed a
Chat Modelnode plus one connected node per tool - Attach a
Window Buffer Memorynode so the agent keeps turn context - Each tool carries a
nameand adescriptionthat tells the model when to use it - The HTTP tool maps its input into the URL via
{{ $fromAI('id') }} - The model — not the wiring — decides which tool fires
💡 Hint: Write the tool descriptions as if the model is your only reader; a vague description is why an agent picks the wrong tool.
Show solution
The AI Agent node takes a model + a set of connected tool nodes; it decides which to call. Graph:
+-- [ Tool: HTTP "get_order" ] (GET /orders/{id})
[ Chat Trigger ] -- [ AI Agent ] --+
| +-- [ Tool: Calculator ]
| (model: Chat Model node)
+-- memory: [ Window Buffer Memory ]
Each tool is declared with a name + description the agent reasons over:
{
"tool": "get_order",
"description": "Look up an order by its id. Input: {\"id\": \"string\"}",
"method": "GET",
"url": "https://api.example.com/orders/{{ $fromAI('id') }}"
}
The jump from LLM node to Agent node is exactly the jump from "one fixed call" to "model chooses tools in a loop" — the description text is what the model uses to pick.
Context: A visual workflow is still a program: its data type is “array of items” and its steps are nodes. Making that mapping explicit is what lets you move fluently between canvas and code.
Your task: Produce the canvas↔code correspondence table, then back it with a n8n Code node snippet that transforms items the way a Set node would.
Requirements:
- Map trigger, node, connection, item,
Set,IF, andLoopeach to a code equivalent - An item (
{json:{...}}) corresponds to a dict in a list - The
Codenode returns a new items array, one dict per item - It renames/derives fields (e.g. builds a full name, lowercases an email) exactly as a
Setnode would
💡 Hint: Return items.map(...) from the Code node — the array of items is the workflow's one data type, so a transform is just a map over it.
Show solution
Canvas ↔ code correspondence:
canvas concept code equivalent
------------------- ---------------------------------
trigger the event/handler that starts main()
node a function step
connection passing the return value onward
item ({json:{...}}) a dict in a list
Set node building/renaming dict fields
IF node an if/branch
Loop / Split In Batches a for-loop over items
An n8n Code node (JavaScript) doing what a Set node does, one item at a time:
// n8n Code node -- runs once, returns the new items array
return items.map(item => ({
json: {
fullName: item.json.first + " " + item.json.last,
email: item.json.email.toLowerCase()
}
}));
Seeing the mapping makes the canvas legible: a workflow is a program whose data type is "array of items" and whose steps are nodes.
Context: Production workflows fail in three ways: transient errors, permanent errors, and replays. Each has its own remedy — retries, an error branch, and an idempotency key.
Your task: Design the per-node reliability settings, a dedicated error-output branch, and a dedup-key strategy that makes the workflow safe to replay.
Requirements:
- Enable
Retry On Failwith a max-tries count and a wait between attempts for flaky nodes - Set
On Errorto continue via the error output and wire it to an alert + mark-failed branch - Build a stable dedup key per event (e.g.
source:id) and skip an event whose key was already seen - Retries cover transient faults; the error branch covers permanent ones; the dedup key covers replays
- Show the dedup logic skipping the second copy of a duplicate event
💡 Hint: Retry, error branch, and dedup are three separate defenses — don't collapse them; the dedup key must be stable across replays, not random per run.
Show solution
Per-node reliability settings + a dedicated error branch:
node settings (HTTP / LLM node):
Retry On Fail: true
Max Tries: 3
Wait Between: 2000 ms (exponential-ish)
On Error: "Continue (error output)" -> wire error output to a branch
error branch:
[ node error output ] --> [ Slack: alert #ops ] --> [ Set: mark row failed ]
Idempotency via a dedup key so replays don't double-process. The logic, modeled offline:
seen = set()
def process(item):
key = f"{item['source']}:{item['id']}" # stable per-event key
if key in seen:
return "skip (duplicate)"
seen.add(key)
return "process"
evts = [{"source":"webhook","id":"A"}, {"source":"webhook","id":"A"},
{"source":"webhook","id":"B"}]
print([process(e) for e in evts]) # ['process', 'skip (duplicate)', 'process']
Retry handles transient failures; the error branch handles permanent ones; the dedup key makes the whole workflow safe to replay.
Context: A real support-triage workflow has to classify inbound email, route by category, and auto-reply or escalate — while logging every path so the behavior is auditable and the LLM never acts irreversibly alone.
Your task: Design the end-to-end workflow from an IMAP Email Trigger to a Switch that routes by category, ending in an append to an audit log; give the full node graph and the routing table.
Requirements:
- An AI Agent classifies each email into
{category, urgency, needs_human} - A
Switchon{{ $json.category }}routes billing, howto, bug, and a default escalation path differently - The LLM only classifies; deterministic nodes perform the actions
- The default/unknown branch escalates to a human channel
- Every path appends a row to a Google Sheet audit log (id, category, action, timestamp)
💡 Hint: Keep the irreversible actions on deterministic nodes downstream of the classifier — the model decides the label, the wiring decides the act.
Show solution
The complete graph:
[ IMAP Email Trigger ]
|
[ AI Agent: classify {category, urgency, needs_human} ]
|
[ Switch on {{ $json.category }} ]
|-- billing --> [ HTTP: create billing ticket ] --> [ Reply: template ]
|-- howto --> [ LLM: draft answer ] --> [ IF urgency=high? ] --> reply / queue
|-- bug --> [ Jira: create issue ] --> [ Reply: ack ]
|-- default --> [ Slack #support: escalate to human ]
|
[ Append row to Google Sheet: audit log (id, category, action, ts) ]
The routing table the Switch encodes, modeled offline:
def route(cls):
table = {"billing": "auto-ticket + templated reply",
"howto": "LLM draft, human review if urgent",
"bug": "file Jira + ack"}
return table.get(cls["category"], "escalate to human")
print(route({"category": "billing"}))
print(route({"category": "unknown"})) # -> escalate
The design principle: the LLM classifies, deterministic nodes act, and every path logs — so behavior is auditable and the LLM never takes an irreversible action alone.
✓ Checkpoint — you can move on when you can…
- Say when a visual workflow beats hand-written code, and when it doesn't.
- Name trigger, node, connection, expression, and credential and what each does.
- Build an LLM workflow with pinned test data and inspect the JSON at each node.
- Turn it into an AI Agent node with a model, a tool, and memory.
- Map every canvas concept to its code equivalent from C2 / Chapter 4.
Knowledge check check yourself
In n8n, what is the whole workflow model in terms of triggers, nodes, and connections, and what travels along the connections?
Show answer
What does the AI Agent node add over a plain Chat Model node, and what three pieces do you attach to it?