Core Concepts and Workflow Patterns#
In the Quick Start notebook, you built a working workflow and learned the basics hands-on. This notebook deepens your understanding and teaches you three powerful workflow patterns.
What’s covered:
How data flows between nodes
Connection types (solid vs dotted lines)
Expressions in more detail
Test mode vs production
Three workflow patterns: Prompt Chaining, Routing, Parallelization
How Data Flows#
In Quick Start, you saw data move from Edit Fields to Basic LLM Chain. Here’s a deeper look at how this works.
Items#
n8n passes data as a list of items. Each item is a JSON object:
[
{ "topic": "coffee", "style": "funny" },
{ "topic": "rain", "style": "sad" }
]
When a node receives 2 items, it processes each one and outputs 2 results.
Input and Output#
Every node has:
Input — what it receives from the previous node
Output — what it sends to the next node
[Node A] ──output──▶ [Node B] ──output──▶ [Node C]
(input) (input)
Click any node after running to see both tabs in the Output Panel.
Connection Types#
You’ve seen two types of lines connecting nodes:
Line Type |
What it means |
Example |
|---|---|---|
Solid line |
Data flows from one node to the next |
Edit Fields → LLM Chain |
Dotted line |
Capability connection (no data flows) |
Chat Model → LLM Chain |
Solid Lines = Data Flow#
When you connect nodes with a solid line, the output of the left node becomes the input of the right node. Data always flows left-to-right.
Dotted Lines = Capabilities#
Dotted lines connect sub-nodes that provide capabilities:
Chat Model → provides AI capability to LLM Chain
Memory → provides conversation history to AI Agent
Tools → provide actions to AI Agent
No data flows through dotted lines. They just say “this node can use that capability.”
Triggers#
Every workflow starts with a Trigger — the leftmost node that defines what starts the workflow.
Trigger |
When it fires |
|---|---|
Manual Trigger |
You click “Execute Workflow” |
Schedule Trigger |
On a timer (hourly, daily, etc.) |
Webhook Trigger |
When another service sends a request |
Chat Trigger |
When a user sends a chat message |
In this course: We use Manual Trigger for most examples. Chat Trigger appears in the AI Agent examples for conversations.
Test Mode vs Activated#
Mode |
How it runs |
When to use |
|---|---|---|
Test |
Click “Execute Workflow” manually |
While building |
Activated |
Runs automatically when triggered |
Production |
Stay in Test mode while learning. This gives you control and lets you use pinned data.
To activate a workflow for production, toggle the Active switch in the top-right. But don’t do this while learning — you might trigger unexpected API calls.
Expressions Deep Dive#
In Quick Start, you used {{ $json.topic }} to pass data. Here’s a closer look at expressions.
The Basics#
Expression |
What it does |
|---|---|
|
Read a field from the previous node |
|
Read a nested field |
|
Mix text with data |
Fixed vs Expression Mode#
Every input field has two modes:
Fixed — literal text,
{{ }}is treated as textExpression —
{{ }}is evaluated as code
If your expression isn’t working, check that you’re in Expression mode (click the field to toggle).
Referencing a Specific Node#
Sometimes you need data from a node that isn’t directly before the current one. There are two equivalent syntaxes:
{{ $node["Edit Fields"].json.topic }}
{{ $('Edit Fields').item.json.topic }}
Both work the same way. The $('Node Name') syntax is newer and shorter. Use whichever you prefer — just be consistent.
You’ll see this pattern in the Routing example, where specialists access the original input directly.
Common Expressions#
Beyond reading fields, n8n has built-in expressions for common tasks.
Date and Time:
Expression |
Result |
|---|---|
|
Current date and time |
|
Today at midnight |
|
Formatted: |
|
Formatted: |
Conditionals:
Expression |
What it does |
|---|---|
|
If-then-else |
|
Default if empty |
Text Manipulation:
Expression |
Result |
|---|---|
|
|
|
|
|
Replace text |
|
Get part before |
Workflow Info:
Expression |
What it returns |
|---|---|
|
Name of current workflow |
|
Unique ID of this run |
|
Current item index (0, 1, 2…) |
Tip: These work anywhere you can write expressions. Combine them: {{ $now.format('yyyy-MM-dd') }}: {{ $json.message.toUpperCase() }}
Quick Troubleshooting#
Problem |
Solution |
|---|---|
Expression shows literally |
Switch from Fixed to Expression mode |
Field not found |
Check spelling and case in JSON view |
Data seems stale |
Check if a node is pinned |
Node has red error |
Click it and read the error in Output Panel |
Core Concepts Summary#
Concept |
Key Point |
|---|---|
Items |
Data flows as a list of JSON objects |
Solid lines |
Data flow between nodes |
Dotted lines |
Capability connections (no data) |
Triggers |
What starts the workflow |
Test mode |
Manual runs while building |
Expressions |
|
Common expressions |
|
Pinning |
Save output to avoid re-running |
Workflow Examples: Three Patterns#
This section demonstrates three common workflow patterns. Import the pre-built workflows, run them, and observe how the nodes work together.
Note on credentials: These workflows require API credentials for an AI provider (OpenRouter, OpenAI, or Google). If you do not have credentials set up, you can still explore the workflow structure and understand what each node would do.
How to Import and Run a Workflow#
Each workflow provides a URL to import it:
Import from URL (recommended):
Copy the URL from the workflow section (starts with
https://raw.githubusercontent.com/...)In n8n, click Workflows → Add Workflow
Click the three-dot menu (⋮) in the top-right
Select Import from URL
Paste the URL and click Import
Click Save
Run:
Open the workflow in the editor
Make sure credentials are set up (Settings → Credentials)
Click Execute Workflow in the top toolbar
Click any node to see its output in the right panel
Cost safety: Keep workflows Inactive (toggle OFF) while learning. After the first successful AI call, Pin data (📌) on that node before editing downstream nodes — this prevents repeated API charges.
Pattern 1: Prompt Chaining#
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ Manual Trigger │────▶│ Edit Fields │────▶│ Basic LLM #1 │────▶│ Basic LLM #2 │──...
└─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘
Import via URL (copy and paste in n8n → Import from URL):
https://raw.githubusercontent.com/ezponda/ai-application-course-materials/main/_static/workflows/01_prompt_chaining.json
Nodes in This Pattern#
This pattern uses nodes you already know from Quick Start:
Node |
What it does |
|---|---|
Manual Trigger |
Starts the workflow when you click “Execute Workflow” |
Edit Fields (Set) |
Creates or transforms data fields |
Basic LLM Chain |
Sends a prompt to an AI model and returns the response |
The difference here: we chain multiple Basic LLM Chains in sequence, where each uses the previous one’s output.
What Problem This Solves#
Breaking a complex writing task into smaller steps improves quality. Instead of asking an LLM to “write a memo” in one shot, you ask it to: (1) create an outline, (2) improve the outline, (3) write the final draft. Each step builds on the previous result.
Node-by-Node Walkthrough#
┌──────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ ┌──────────────────┐
│ Manual Trigger │────▶│ Input — Writing Brief │────▶│ Step 1 — Outline │────▶│ Store Outline │
└──────────────────┘ └────────────────────────┘ └────────────────────────┘ └──────────────────┘
│
┌───────────────────────────────────────────────────────────────────────────────────────┘
▼
┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐ ┌────────────────────────┐
│ Step 2 — Improve │────▶│ Store Improved │────▶│ Step 3 — Draft Memo │────▶│ Output — Final Memo │
└────────────────────────┘ └────────────────────────┘ └────────────────────────┘ └────────────────────────┘
Node |
Type |
What it does |
|---|---|---|
Run: Prompt Chaining |
Manual Trigger |
Starts the workflow |
Input — Writing Brief |
Set |
Creates fields: |
Step 1 — Create Outline |
Basic LLM Chain |
Generates an outline → outputs |
Store Outline |
Set |
Saves |
Step 2 — Improve Outline |
Basic LLM Chain |
Refines the outline using |
Store Improved Outline |
Set |
Saves |
Step 3 — Draft Memo |
Basic LLM Chain |
Writes final memo using |
Output — Final Memo |
Set |
Saves |
Sub-node: One OpenRouter Chat Model is shared by all three LLM Chain nodes (connected via dotted lines).
Prompts Used#
Each step uses a different role and rules. This is why chaining works better than one big prompt.
Step 1 — Create Outline:
System: You are a writing assistant.
Create a clear outline for the memo.
Rules:
- 5–7 sections
- Include where the benefits bullets and risks bullet will go
- Output ONLY the outline
Step 2 — Improve Outline:
System: You are a strict editor.
Improve the outline using this checklist:
- Logical flow
- Clear headings
- Covers constraints (benefits + risks)
- Non-technical wording
Output ONLY the improved outline (no commentary).
Step 3 — Draft Memo:
System: You are a business writer.
Write the memo based on the outline.
Rules:
- Follow the outline order
- Keep under 400 words
- Use clear, non-technical language
- Include exactly: 3 benefit bullets + 1 risk bullet
Output ONLY the memo text.
Why this works: Each step has a focused role (assistant → editor → writer), specific constraints, and ends with “Output ONLY…” to prevent extra commentary.
Data Flow#
INPUT OUTPUT
───── ──────
Trigger: { }
↓
Writing Brief: { topic, audience, constraints }
↓
Step 1 LLM: { text: "1. Introduction..." }
↓
Store Outline: { outline: "1. Introduction..." }
↓
Step 2 LLM: { text: "1. Executive Summary..." }
↓
Store Improved: { improved_outline: "1. Executive Summary..." }
↓
Step 3 LLM: { text: "MEMO: Why We Should..." }
↓
Final Output: { final_memo: "MEMO: Why We Should..." }
What to Observe#
Click Input — Writing Brief → see the starting data
Click Step 1 — Create Outline → see the LLM’s outline in
textClick Store Outline → see the same content now saved as
outlineFollow this pattern through each step to see how data transforms
Why do we need “Store” nodes?#
When a Basic LLM Chain runs, it replaces the previous data with only its output. This diagram shows what happens:
STEP 1: Input STEP 2: LLM generates outline STEP 3: Store saves it
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ Input — Writing │─────────▶│ Step 1 — Outline │────────────▶│ Store Outline │
│ Brief │ │ (Basic LLM Chain) │ │ (Edit Fields) │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────┐ ┌─────────────────────┐ ┌─────────────────────┐
│ { │ │ { │ │ { │
│ "topic": "AI", │ │ "text": │ │ "text": │
│ "audience": │ │ "1. Intro │ │ "1. Intro...", │
│ "doctors", │ │ 2. Benefits │ │ "outline": │
│ "constraints": │ │ 3. Risks..." │ │ "1. Intro │
│ "benefits, │ │ } │ │ 2. Benefits │
│ risks" │ │ │ │ 3. Risks..." │
│ } │ │ ⚠️ topic, audience, │ │ } │
│ │ │ constraints are │ │ │
│ │ │ GONE! │ │ 💡 Store adds │
│ │ │ │ │ "outline" field │
└─────────────────────┘ └─────────────────────┘ └─────────────────────┘
Key insight:
The LLM outputs only
{ "text": "..." }— original fields are lostThe Store node adds
outlineto save the result with a meaningful nameStep 2 only needs
{{ $json.outline }}so it works without the original fields
Why this pattern? Each LLM step has a focused task. Step 2 doesn’t need topic or audience — it only needs the outline to improve it.
Pattern 2: Routing#
┌─────────────────┐
┌───▶│ LLM: Refund │
┌─────────────────┐ ┌─────────────────┐ │ └─────────────────┘
│ Manual Trigger │────▶│ LLM: Classify │─┼───▶│ LLM: Order │
└─────────────────┘ └─────────────────┘ │ └─────────────────┘
└───▶│ LLM: Support │
└─────────────────┘
Import via URL (copy and paste in n8n → Import from URL):
https://raw.githubusercontent.com/ezponda/ai-application-course-materials/main/_static/workflows/02_routing.json
Meet the Node: Switch#
This pattern introduces a new node: Switch.
Property |
Description |
|---|---|
Purpose |
Routes data to different branches based on conditions |
How it works |
Checks a value and sends data to the matching output |
Multiple outputs |
Each condition creates a separate output branch (solid line) |
Configuration:
Mode: Choose “Rules” for condition-based routing
Data Type: Usually “String” when routing on text values
Value 1: The field to check, e.g.,
{{ $json.route }}Operation: Usually “Equals” for exact matching
Value 2: The value to match, e.g.,
refund
Add more rules to create more branches. The Switch sends data only to the branch where the condition matches.
What Problem This Solves#
Different inputs need different handling. A support ticket about refunds should go to a refund specialist prompt, while an order status question goes elsewhere. Routing uses an LLM to classify the input, then a Switch node sends it down the right path.
Node-by-Node Walkthrough#
┌──────────────────┐ ┌────────────────────────┐ ┌──────────────────────┐ ┌───────────────┐ ┌────────────────┐
│ Manual Trigger │────▶│ Input — Support Ticket │────▶│ Router — Classify │────▶│ Store Route │────▶│ Switch — Route │───┐
└──────────────────┘ └────────────────────────┘ └──────────────────────┘ └───────────────┘ └────────────────┘ │
│
┌───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
│
├────▶┌─────────────────────────────┐────▶┌───────────────────────┐
│ │ Refund Specialist │ │ Output — Refund │
│ └─────────────────────────────┘ └───────────────────────┘
│
├────▶┌─────────────────────────────┐────▶┌───────────────────────┐
│ │ Order Status Specialist │ │ Output — Order │
│ └─────────────────────────────┘ └───────────────────────┘
│
└────▶┌─────────────────────────────┐────▶┌───────────────────────┐
│ Support Specialist │ │ Output — Support │
└─────────────────────────────┘ └───────────────────────┘
Node |
Type |
What it does |
|---|---|---|
Run: Ticket Routing |
Manual Trigger |
Starts the workflow |
Input — Support Ticket |
Set |
Creates fields: |
Router — Choose Route |
Basic LLM Chain |
Classifies ticket → outputs |
Store Route |
Set |
Saves |
Switch — Route |
Switch |
Checks |
Refund Specialist |
Basic LLM Chain |
Writes refund-specific reply → outputs |
Order Status Specialist |
Basic LLM Chain |
Writes order status reply → outputs |
Support Specialist |
Basic LLM Chain |
Writes general support reply → outputs |
Prompts Used#
Router — Choose Route (the classifier):
System: You route customer support tickets.
Choose exactly ONE route label, lowercase, no punctuation:
- refund
- order_status
- support
Return ONLY the label.
Why this works: The prompt forces clean output (lowercase, no punctuation, ONLY the label) so the Switch node can match exactly.
Refund Specialist:
System: You are a customer support specialist for refunds.
Write a short, professional reply.
Rules:
- Acknowledge the issue
- Ask for any missing info (only if needed)
- Explain next steps and expected timeline
- Keep it under 120 words
Output ONLY the reply.
Key Expression: Accessing Earlier Node Data#
The specialist nodes need the original ticket data, but they come after the Switch. They use this expression pattern:
{{ $node['Input — Support Ticket'].json.ticket_subject }}
{{ $node['Input — Support Ticket'].json.ticket_body }}
This accesses data from a specific earlier node by name. Useful when you need to “reach back” past intermediate nodes.
How does the Switch node route data?#
The Switch node checks a field value and sends data to only one matching branch:
┌─────────────────────────────────────────────┐
│ DATA BEFORE SWITCH │
│ { │
│ "text": "refund", │
│ "route": "refund" │
│ } │
└─────────────────────────────────────────────┘
│
▼
┌───────────────────┐
│ SWITCH │
│ Checks: route │
└───────────────────┘
/ | \
"refund" / "order_status" \ "support"
/ | \
▼ ▼ ▼
┌──────────────────┐ ┌──────────────┐ ┌──────────────┐
│ ✅ HAS DATA │ │ ❌ EMPTY │ │ ❌ EMPTY │
└──────────────────┘ └──────────────┘ └──────────────┘
Key insights:
Only ONE branch receives data — the others are empty
Specialist nodes “reach back” to get original data using
$node['Input — Support Ticket'].json
Data Flow#
INPUT OUTPUT
───── ──────
Trigger: { }
↓
Support Ticket: { ticket_subject, ticket_body }
↓
Router LLM: { text: "refund" }
↓
Store Route: { text: "refund", route: "refund" }
↓
Switch: routes to ONE branch based on {{ $json.route }}
↓
[Refund Specialist]: { text: "Dear Jamie, I apologize..." }
↓ (accesses original ticket via $node['Input — Support Ticket'])
Output — Refund Reply: { reply: "Dear Jamie, I apologize...", route: "refund" }
What to Observe#
Click Router — Choose Route → see the classification in
text(e.g.,refund)Click Store Route → see
routefield addedClick Switch — Route → only ONE output branch has data
Click the active specialist node → see the tailored reply
Pattern 3: Parallelization#
┌─────────────────┐
┌───▶│ LLM: Facts │───┐
┌─────────────────┐│ └─────────────────┘ │ ┌─────────────────┐
│ Manual Trigger │┼───▶│ LLM: Sentiment │───┼───▶│ Merge │────▶ ...
└─────────────────┘│ └─────────────────┘ │ └─────────────────┘
└───▶│ LLM: Draft │───┘
└─────────────────┘
Import via URL (copy and paste in n8n → Import from URL):
https://raw.githubusercontent.com/ezponda/ai-application-course-materials/main/_static/workflows/03_parallelization.json
Meet the Node: Merge#
This pattern introduces a new node: Merge.
Property |
Description |
|---|---|
Purpose |
Combines data from multiple branches into one |
Multiple inputs |
Receives data from 2+ parallel branches |
Output |
One combined item (or list) with all fields |
Key Modes:
Mode |
What it does |
|---|---|
Combine by Position |
Pairs items by index (1st + 1st, 2nd + 2nd). Best when each branch outputs one item. |
Combine by Fields |
Matches items by a common field value |
Append |
Puts all items into one list |
In this workflow, we use Combine by Position because each branch outputs exactly one item.
What Problem This Solves#
Some tasks have independent parts that can be analyzed separately. Analyzing a customer email for facts, sentiment, and drafting a reply are independent—they don’t need each other’s results. Splitting them into branches keeps your workflow organized, and you can combine the results at the end for a final, informed response.
Not true parallel execution. Despite the visual layout, n8n executes branches sequentially (A, then B, then C), not simultaneously. True parallel execution would require sub-workflows with webhook triggers — significantly more complex to set up.
This pattern is still valuable for code organization and clarity, even without the speed benefit of real parallelization.
Node-by-Node Walkthrough#
┌──────────────────┐ ┌────────────────────────┐
│ Manual Trigger │────▶│ Input — Customer Email │───┬────▶ Branch A — Facts ────▶ Store Facts ────┐
└──────────────────┘ └────────────────────────┘ │ │
├────▶ Branch B — Sentiment ────▶ Store Sentiment ├──▶ Merge ──▶ Finalize
│ │
└────▶ Branch C — Draft ────▶ Store Draft ────┘
Node |
Type |
What it does |
|---|---|---|
Input — Customer Email |
Set |
Creates fields: |
Branch A — Extract Facts |
Basic LLM Chain |
Extracts facts as JSON → outputs |
Branch B — Sentiment & Urgency |
Basic LLM Chain |
Analyzes sentiment → outputs |
Branch C — Draft Reply |
Basic LLM Chain |
Drafts initial reply → outputs |
Merge |
Merge |
Combines all three fields (mode: Combine by Position) |
Finalize — Improved Reply |
Basic LLM Chain |
Uses all three fields → outputs |
Prompts Used#
Branch A — Extract Facts:
System: Extract key facts from the email.
Return STRICT JSON with keys:
customer_name, issue, deadline, requested_action, missing_info (array).
Return JSON only.
Branch B — Sentiment & Urgency:
System: Classify sentiment and urgency.
Return STRICT JSON with keys:
sentiment (positive|neutral|negative), urgency (low|medium|high), risk_flags (array).
Return JSON only.
Branch C — Draft Reply:
System: Draft a helpful customer support email reply.
Rules:
- Friendly and concise
- Ask for missing info only if needed
- Offer 1–2 concrete next steps
- Under 140 words
Output ONLY the reply text.
Finalize — One Improved Reply:
System: You are a senior support agent.
You will receive:
- facts_json (extracted facts)
- sentiment_json (sentiment & urgency)
- draft_reply (initial draft)
Task:
1) Improve the draft to match urgency and include any critical missing info questions.
2) Keep it under 160 words.
3) Output ONLY the final reply text.
How does Merge combine branches?#
The Merge node waits for all branches to complete, then combines their outputs into one item.
┌─────────────────────────────────┐
│ INPUT (same for all) │
│ { "email_subject": "Help!", │
│ "email_body": "Can't login" } │
└─────────────────────────────────┘
│
┌──────────────┼──────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Branch A │ │ Branch B │ │ Branch C │
│ (Facts) │ │ (Sentiment) │ │ (Draft) │
│ runs 1st │ │ runs 2nd │ │ runs 3rd │
└──────────────┘ └──────────────┘ └──────────────┘
│ │ │
└──────────────┼──────────────┘
▼
┌─────────────────┐
│ MERGE │
│ (Combine by │
│ Position) │
└─────────────────┘
│
▼
┌─────────────────────────────────┐
│ MERGED OUTPUT │
│ { │
│ "facts_json": "{...}", │
│ "sentiment_json": "{...}", │
│ "draft_reply": "Hi Sam..." │
│ } │
│ │
│ ✅ All three fields combined! │
└─────────────────────────────────┘
Key insight: Even though execution is sequential, the pattern is valuable for organizing independent analyses. The Merge node combines all results so the final LLM can use all the information.
Data Flow#
INPUT OUTPUT
───── ──────
Trigger: { }
↓
Customer Email: { email_subject, email_body }
↓ ↓ ↓ (splits into 3 branches — executed sequentially)
Branch A: { text: '{"customer_name":"Sam",...}' }
↓
Store Facts: { facts_json: '{"customer_name":"Sam",...}' }
Branch B: { text: '{"sentiment":"negative","urgency":"high",...}' }
↓
Store Sentiment: { sentiment_json: '{"sentiment":"negative",...}' }
Branch C: { text: "Hi Sam, I understand..." }
↓
Store Draft Reply: { draft_reply: "Hi Sam, I understand..." }
↓ (merge all three)
After Merge: { facts_json, sentiment_json, draft_reply }
↓
Finalize LLM: { text: "Dear Sam, I sincerely apologize..." }
↓
Final Output: { final_reply: "Dear Sam, I sincerely apologize..." }
What to Observe#
Click Input — Customer Email → see the starting data
Click each Branch node → see different analyses running on the same input
Click Merge → switch to JSON view to see all three fields combined
Click Finalize → see how the final LLM uses all three fields
Pattern Summary#
Pattern |
When to use |
Key nodes |
|---|---|---|
Prompt Chaining |
Complex tasks that benefit from step-by-step refinement |
Multiple LLM Chains in sequence |
Routing |
Different handling based on input type |
LLM (classifier) + Switch |
Parallelization |
Independent analyses that can run simultaneously |
Multiple branches + Merge |
Tips for Building Your Own Workflows#
Start simple. Build one node at a time and test frequently.
Use descriptive node names. “Step 1: Create Outline” is better than “LLM Chain.”
Pin data often. After any successful LLM call, pin the result before working on the next node.
Check the Output panel. Switch between Table and JSON views to understand your data.
Use Set nodes as checkpoints. Save intermediate results with clear field names.
Test routing with different inputs. Make sure all branches work, not just the happy path.
Recap#
In this notebook, you learned:
Core Concepts:
Data flows as JSON items through solid lines
Dotted lines connect capabilities (Chat Model, Memory, Tools)
Expressions (
{{ $json.field }}) pass data between nodesPinning saves output to avoid re-running expensive operations
Three Workflow Patterns:
Prompt Chaining: Break complex tasks into sequential LLM steps
Routing: Classify input and route to specialized handlers with Switch
Parallelization: Run independent analyses and merge results
These patterns are your foundation for building more sophisticated AI systems with n8n.