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
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.
| 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
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
- 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
Start simple: a workflow that classifies an incoming message. This is the visual twin of Chapter 2's classifier.
- 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
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
| 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
| 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?