Building a Multi-Agent Workflow With n8n: Orchestrator, Research Agent, and Writer Agent
Most n8n "multi-agent" tutorials online wire up one AI Agent node with a web search tool and call it a day. That's not multi-agent — that's a single agent with tools. Real multi-agent architecture means distinct agents with distinct roles, their own system prompts, their own tool access, and a coordination layer (the orchestrator) that decides which agent runs, when, and what context to pass.
This tutorial builds a production-ready three-agent pipeline:
- Orchestrator — parses the incoming task, routes to sub-agents, validates outputs, synthesises the final result
- Research Agent — web search specialist; takes a topic and returns structured JSON findings
- Writer Agent — content specialist; takes research findings and requirements, returns formatted markdown
When we built this at Innovatrix Infotech for a client's automated content pipeline, the full pipeline — from task ingress to formatted draft — runs in under 90 seconds and costs approximately $0.06–$0.08 per run at GPT-4o pricing. The client previously paid a part-time researcher 3–4 hours per article. That's the ROI case in one sentence.
This tutorial is completable in 60–90 minutes. You need working knowledge of n8n — creating nodes, connecting sub-nodes, managing credentials. If you've never touched n8n, read the official intro tutorial first.
Prerequisites
| Requirement | Details |
|---|---|
| n8n version | 1.54+ (AI Agent Tool node requires 1.40+) |
| Deployment | Self-hosted Docker or n8n Cloud |
| LLM credentials | OpenAI API key (GPT-4o for orchestrator + writer, GPT-4o-mini for research) |
| Web search API | Brave Search API key (free tier: 2,000 queries/month) |
| Node.js (self-hosted) | 20.x LTS |
Why GPT-4o for orchestrator and writer, GPT-4o-mini for research? The orchestrator and writer need strong reasoning and coherent long-form output — tasks where model quality has a direct quality ceiling. The research agent runs narrower, more repeatable structured extraction tasks where GPT-4o-mini performs near-identically at ~10x lower cost. This split reduces total per-run cost by roughly 65%.
Architecture Overview
Before touching a single node, understand what you're building:
POST /webhook/content-pipeline
│
▼
┌──────────────────────────────┐
│ MAIN WORKFLOW │
│ (Orchestrator) │
│ │
│ Webhook Trigger │
│ │ │
│ Code: Parse + Validate Input │
│ │ │
│ Execute Sub-Workflow ────────┼──► RESEARCH AGENT SUB-WORKFLOW
│ [Research Agent] │ └── Returns: JSON findings
│ │ │
│ IF: Research succeeded? │
│ │ (true) │
│ Code: Merge context │
│ │ │
│ Execute Sub-Workflow ────────┼──► WRITER AGENT SUB-WORKFLOW
│ [Writer Agent] │ └── Returns: markdown string
│ │ │
│ Code: Final validation │
│ │ │
│ Respond to Webhook │
└──────────────────────────────┘
Key architectural decision: sub-agents are separate workflows called via Execute Sub-Workflow nodes — not AI Agent Tool nodes that call agents as tools. This makes each sub-workflow independently testable, independently deployable, and independently debuggable. You can update the Research Agent's system prompt without touching the orchestrator's logic.
Step 1: Set Up Credentials
Navigate to Settings → Credentials and add:
OpenAI:
- Name:
OpenAI Production - Type: OpenAI API
- API Key: your key
Brave Search:
- Name:
Brave Search - Type: HTTP Header Auth
- Header Name:
X-Subscription-Token - Header Value: your Brave Search API key
Use consistent names. Credential names are referenced by string in nodes — an inconsistency here breaks export/import between environments.
Step 2: Build the Research Agent Sub-Workflow
Create a new workflow. Name it exactly Research Agent (this name is referenced in the orchestrator).
2.1 — Execute Sub-Workflow Trigger
Add an Execute Sub-Workflow Trigger node.
Input Data Mode: Define using fields and single item
Add one input field:
- Field Name:
research_input - Type: Object
Expected schema:
{
"research_input": {
"topic": "string",
"context": "string",
"depth": "standard | comprehensive",
"max_sources": 3,
"target_audience": "string"
}
}
2.2 — Input Validation (Code Node)
Add a Code node immediately after the trigger:
// Validate and normalise research input
const input = $input.first().json.research_input;
if (!input?.topic) {
throw new Error('Research input missing required field: topic');
}
return [{
json: {
topic: String(input.topic).trim(),
context: String(input.context || '').trim(),
depth: ['standard', 'comprehensive'].includes(input.depth)
? input.depth
: 'standard',
max_sources: Math.min(Math.max(parseInt(input.max_sources) || 3, 1), 10),
target_audience: String(input.target_audience || 'technical professionals').trim(),
timestamp: new Date().toISOString()
}
}];
Never skip input validation in sub-workflows. The orchestrator's AI Agent constructs tool call arguments, and LLMs occasionally produce malformed JSON or omit optional fields. Validate at every boundary.
2.3 — Brave Search HTTP Request Tool
Add an HTTP Request node. Configure it as a tool by connecting it to the AI Agent node's ai_tool connector (not the main data path).
Method: GET
URL: https://api.search.brave.com/res/v1/web/search
Query parameters:
| Parameter | Value |
|---|---|
q |
{{ $fromAI("search_query", "The specific web search query to execute") }} |
count |
10 |
freshness |
pm (past month) |
text_decorations |
false |
search_lang |
en |
Authentication:
Auth Type: Predefined Credential Type
Credential Type: HTTP Header Auth
Credential: Brave Search
The $fromAI() call is critical. This is how n8n lets the LLM populate tool parameters at runtime. When the Research Agent decides to call this tool, it fills in search_query. The description string is what the LLM reads to understand what to put there — write it clearly.
2.4 — URL Content Fetcher Tool (Recommended)
Add a second HTTP Request tool node for cases where search snippets aren't sufficient:
Method: GET
URL: {{ $fromAI("url", "The full URL of a specific webpage to retrieve and read") }}
Response Format: Text
Connect this as a second ai_tool to the Research Agent. The agent uses it when it needs the full page content of a promising result.
Security note: Add a Code node after this tool to check content-length and strip any script tags before the content is passed back to the agent. Runaway agents will occasionally attempt to fetch API endpoints or large PDFs — both cause execution timeouts or token explosions.
2.5 — Configure the Research Agent AI Node
Add an AI Agent node. Connect:
ai_languageModel→ OpenAI Chat Model node (model:gpt-4o-mini-2024-07-18, temperature:0)ai_tool→ Brave Search HTTP Requestai_tool→ URL Fetcher HTTP Request
Agent: Tools Agent
Max Iterations: 8
System Prompt:
You are a specialist research agent. Your role is to gather accurate,
current, and well-sourced information about a given topic.
You have two tools available:
1. brave_search — search the web for current information
2. fetch_url — retrieve the full text content of a specific webpage
RESEARCH PROTOCOL:
1. Execute 2–3 targeted search queries covering different angles of the topic
2. Identify the 3–5 most relevant, authoritative sources
3. Fetch 1–2 full pages when snippets are insufficient for depth
4. Synthesise all findings into the required output schema
OUTPUT REQUIREMENTS:
Return ONLY a valid JSON object matching this exact schema.
No markdown. No preamble. No explanation. Just the JSON:
{
"topic": "<exact topic as provided>",
"summary": "<2–3 sentence executive summary>",
"key_findings": [
"<specific finding with evidence>"
],
"statistics": [
{
"stat": "<specific number or percentage>",
"source": "<source name>",
"url": "<source URL>",
"date": "<publication or data date>"
}
],
"sources": [
{
"title": "<page title>",
"url": "<URL>",
"relevance": "<why this source matters>"
}
],
"gaps": [
"<topic area where information was scarce or conflicting>"
],
"confidence": "<high | medium | low>"
}
CRITICAL: If you cannot return valid JSON matching this exact schema,
return {"error": "research_failed", "reason": "<explanation>"}
Prompt input:
Research the following topic thoroughly:
Topic: {{ $json.topic }}
Context: {{ $json.context }}
Target Audience: {{ $json.target_audience }}
Depth: {{ $json.depth }}
Maximum Sources: {{ $json.max_sources }}
Why temperature: 0 for research? You want deterministic, factual output. A research agent that adds creative variation to its structured JSON output is a research agent you cannot trust downstream.
2.6 — Output Validation (Code Node)
Add a Code node after the AI Agent:
// Validate and clean Research Agent output
const rawOutput = $input.first().json.output;
let parsed;
try {
// Handle cases where the LLM wraps JSON in markdown code fences
const cleaned = rawOutput
.replace(/^```json\s*/i, '')
.replace(/^```\s*/i, '')
.replace(/\s*```$/i, '')
.trim();
parsed = JSON.parse(cleaned);
} catch (e) {
return [{
json: {
success: false,
error: 'output_parse_error',
raw: rawOutput,
message: `Research Agent returned non-JSON output: ${e.message}`
}
}];
}
// Validate required fields
const required = ['topic', 'summary', 'key_findings', 'sources'];
const missing = required.filter(field => !parsed[field]);
if (missing.length > 0) {
return [{
json: {
success: false,
error: 'schema_validation_error',
missing_fields: missing,
partial_data: parsed
}
}];
}
// Check for structured error from agent
if (parsed.error) {
return [{
json: {
success: false,
error: parsed.error,
reason: parsed.reason
}
}];
}
return [{
json: {
success: true,
research: parsed,
validated_at: new Date().toISOString()
}
}];
This validation node is the difference between a prototype and a system you can operate. When this fails in your logs, you know the prompt needs tuning — not that your orchestrator crashed with an opaque error.
Your Research Agent sub-workflow is complete. Activate it — sub-workflows must be active to be callable.
Step 3: Build the Writer Agent Sub-Workflow
Create a second workflow. Name it Writer Agent.
3.1 — Execute Sub-Workflow Trigger
Add an Execute Sub-Workflow Trigger.
Input field:
- Field Name:
writer_input - Type: Object
Expected schema:
{
"writer_input": {
"task_title": "string",
"content_type": "blog_post | summary | report | brief",
"target_audience": "string",
"tone": "technical | conversational | authoritative",
"target_word_count": 1500,
"research": {},
"requirements": ["string"],
"internal_links": [{"text": "string", "url": "string"}]
}
}
3.2 — Input Normalisation (Code Node)
const input = $input.first().json.writer_input;
if (!input?.task_title || !input?.research) {
throw new Error('Writer input missing required fields: task_title, research');
}
// Format research data for prompt injection
const researchText = JSON.stringify(input.research, null, 2);
// Format requirements as numbered list
const requirementsList = (input.requirements || [])
.map((r, i) => `${i + 1}. ${r}`)
.join('\n');
// Format internal links for embedding instructions
const internalLinksText = (input.internal_links || [])
.map(l => `- Anchor text: "${l.text}" → URL: ${l.url}`)
.join('\n') || 'None provided';
return [{
json: {
task_title: input.task_title,
content_type: input.content_type || 'blog_post',
target_audience: input.target_audience || 'developers',
tone: input.tone || 'technical',
target_word_count: Math.min(Math.max(parseInt(input.target_word_count) || 1000, 200), 5000),
research_json: researchText,
requirements_list: requirementsList,
internal_links_text: internalLinksText,
timestamp: new Date().toISOString()
}
}];
3.3 — Configure the Writer Agent AI Node
Add an AI Agent node.
ai_languageModel→ OpenAI Chat Model (model:gpt-4o, temperature:0.4)
Why temperature 0.4 for the writer? Readable variation matters here. Temperature 0 produces robotic, repetitive prose. Temperature 0.4 gives you natural sentence variety without drift into hallucination.
Agent: Tools Agent (no tools needed)
Max Iterations: 3
System Prompt:
You are a specialist content writer. You produce high-quality,
well-structured content by transforming research data into formatted output.
WRITING PRINCIPLES:
1. Lead with the most compelling insight — not background context
2. Every factual claim must be traceable to the provided research data
3. Use statistics with proper attribution (Source Name, year)
4. Embed internal links naturally in body text — never as footnotes
5. Headers should be descriptive, not clever
6. Banned transitions: "Furthermore", "Moreover", "In conclusion",
"In today's world", "It is important to", "Without further ado"
7. Write for the specific audience — adjust technical depth accordingly
OUTPUT FORMAT:
Return ONLY the formatted markdown content.
No meta-commentary. No "here is your article". No preamble.
Begin directly with the first heading or paragraph.
If the research data is insufficient for a required section, note it:
[NOTE: Insufficient data — recommend additional research on: <topic>]
Prompt input:
Write a {{ $json.content_type }} with the following specifications:
TITLE: {{ $json.task_title }}
TARGET AUDIENCE: {{ $json.target_audience }}
TONE: {{ $json.tone }}
TARGET LENGTH: approximately {{ $json.target_word_count }} words
CONTENT REQUIREMENTS:
{{ $json.requirements_list }}
INTERNAL LINKS TO EMBED:
{{ $json.internal_links_text }}
Use the exact anchor text provided. Embed them naturally in body text.
RESEARCH DATA:
{{ $json.research_json }}
3.4 — Output Processing (Code Node)
const output = $input.first().json.output;
if (!output || output.trim().length < 100) {
return [{
json: {
success: false,
error: 'insufficient_output',
message: 'Writer Agent returned empty or very short content',
raw: output
}
}];
}
// Word count estimate
const wordCount = output.trim().split(/\s+/).length;
// Count any insufficiency flags the writer added
const insufficiencyFlags = (output.match(/\[NOTE: Insufficient data/g) || []).length;
return [{
json: {
success: true,
content: output,
word_count: wordCount,
insufficiency_flags: insufficiencyFlags,
generated_at: new Date().toISOString()
}
}];
Activate this workflow. Writer Agent is complete.
Step 4: Build the Orchestrator (Main Workflow)
Create a third workflow. Name it Content Pipeline — Orchestrator.
4.1 — Webhook Trigger
Add a Webhook node:
HTTP Method: POST
Path: content-pipeline
Authentication: Header Auth
Response Mode: Using Respond to Webhook Node
For Header Auth, create a credential:
- Header Name:
X-Pipeline-Secret - Header Value: a long random string you generate
Anyone with this key can trigger the pipeline. Treat it like an API key.
Request body shape your clients send:
{
"task": {
"title": "React Server Components vs Client Components: When to Use Which",
"type": "comparison_post",
"target_audience": "React developers",
"tone": "technical",
"word_count": 1800,
"requirements": [
"Cover performance implications with Lighthouse score data",
"Include concrete code examples for both patterns",
"Address the mental model shift from SPA to RSC"
],
"internal_links": [
{"text": "web development services", "url": "/services/web-development"},
{"text": "Next.js development", "url": "/services/nextjs-development"}
]
}
}
4.2 — Parse and Validate Input (Code Node)
const body = $input.first().json.body;
if (!body?.task?.title) {
return [{
json: {
error: true,
code: 'INVALID_INPUT',
message: 'Request body must include task.title'
}
}];
}
const task = body.task;
return [{
json: {
title: String(task.title).trim(),
type: task.type || 'blog_post',
target_audience: task.target_audience || 'technical professionals',
tone: task.tone || 'technical',
word_count: parseInt(task.word_count) || 1500,
requirements: Array.isArray(task.requirements) ? task.requirements : [],
internal_links: Array.isArray(task.internal_links) ? task.internal_links : [],
request_id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
received_at: new Date().toISOString()
}
}];
4.3 — Call Research Agent
Add an Execute Sub-Workflow node:
Source: Database
Workflow: Research Agent
Wait for Sub-Workflow: true
Input data:
{
"research_input": {
"topic": "={{ $json.title }}",
"context": "={{ $json.type }} targeting {{ $json.target_audience }}",
"depth": "comprehensive",
"max_sources": 5,
"target_audience": "={{ $json.target_audience }}"
}
}
4.4 — Validate Research Output (IF Node)
Add an IF node:
Condition 1: {{ $json.success }} is true
On the false branch, add a Code node then Respond to Webhook:
// False branch — Research failed
return [{
json: {
error: true,
code: 'RESEARCH_FAILED',
request_id: $('Parse Input').first().json.request_id,
details: $input.first().json
}
}];
4.5 — Merge Task + Research (Code Node)
On the true branch, combine the original task data with research output:
// Access original task data from the earlier node
const taskData = $('Parse Input').first().json;
const researchData = $input.first().json.research;
return [{
json: {
writer_input: {
task_title: taskData.title,
content_type: taskData.type,
target_audience: taskData.target_audience,
tone: taskData.tone,
target_word_count: taskData.word_count,
research: researchData,
requirements: taskData.requirements,
internal_links: taskData.internal_links
},
request_id: taskData.request_id
}
}];
Important: $('Parse Input').first().json references the output of the node named "Parse Input" directly, regardless of where you are in the execution graph. This is how you access data from earlier workflow nodes in n8n — by name, not by position.
4.6 — Call Writer Agent
Add a second Execute Sub-Workflow node:
Source: Database
Workflow: Writer Agent
Wait for Sub-Workflow: true
Pass writer_input from the Merge node through directly.
4.7 — Final Validation + Response
Add a Code node:
const writerOutput = $input.first().json;
const taskData = $('Parse Input').first().json;
if (!writerOutput.success) {
return [{
json: {
error: true,
code: 'WRITING_FAILED',
request_id: taskData.request_id,
details: writerOutput
}
}];
}
return [{
json: {
success: true,
request_id: taskData.request_id,
title: taskData.title,
content: writerOutput.content,
word_count: writerOutput.word_count,
insufficiency_flags: writerOutput.insufficiency_flags,
metadata: {
model_orchestrator: 'n/a — sequential pipeline',
model_research: 'gpt-4o-mini',
model_writer: 'gpt-4o',
generated_at: new Date().toISOString()
}
}
}];
Add a Respond to Webhook node:
Respond With: JSON
Response Body: {{ $json }}
Response Code: 200 (success path) / 422 (error paths)
Step 5: Cost Per Run
Understanding cost is non-negotiable for production pipelines. Here's the breakdown for a 1,500-word blog post:
| Agent | Model | Est. Input Tokens | Est. Output Tokens | Cost (USD) |
|---|---|---|---|---|
| Orchestrator (parsing) | Code node (no LLM) | — | — | $0.000 |
| Research Agent | GPT-4o-mini | ~3,500 | ~1,200 | ~$0.008 |
| Writer Agent | GPT-4o | ~5,000 | ~2,200 | ~$0.063 |
| Total | ~8,500 | ~3,400 | ~$0.071 |
Pricing at GPT-4o ($5/M input, $15/M output) and GPT-4o-mini ($0.15/M input, $0.60/M output) as of early 2026.
Cost optimisation options:
- Swap Writer Agent to Claude 3.5 Haiku: ~$0.01/run (significant quality tradeoff on long-form)
- Cache research output for related articles: if you're writing 3 articles on the same topic, run research once
- Reduce Research Agent
max_sourcesfrom 5 to 3: saves ~20% on research token cost - Set Research Agent
maxIterationsto 5 (default is 10): prevents runaway tool-use loops
Step 6: Adding Agent Memory (Redis)
By default, every execution is stateless. This is fine for a content pipeline where each task is independent. But for conversational multi-agent systems — where an agent needs to remember it spoke to this user before — you need persistence.
n8n's memory options, ranked by complexity and cost:
| Option | Persistence | Infrastructure | Use Case |
|---|---|---|---|
| Window Buffer Memory | Per-execution only | None | Multi-turn within a single run |
| Redis Chat Memory | Cross-execution | Redis instance | Session-based agent memory |
| Postgres Chat Memory | Cross-execution | PostgreSQL | Queryable conversation history |
| Custom (Code Node + DB) | Full control | Any database | Production systems with audit requirements |
To add Redis-backed memory to the orchestrator:
- Add a Redis Chat Memory node
- Connect to the AI Agent
ai_memoryconnector - Set Session ID Key:
{{ $json.request_id }}(or a stable client identifier) - Set Window Size: 10 (last 10 message pairs)
Critical: Do not add memory to the Research and Writer agents. Keep them stateless. Cross-contamination of context between different research tasks causes subtle hallucination issues — the agent "remembers" a statistic from a previous run and injects it incorrectly into unrelated research. We learned this the hard way on a client pipeline that was confidently producing research summaries sprinkled with unrelated data points from prior runs.
Step 7: Error Handling
Node-Level Retry
For the Execute Sub-Workflow nodes, configure:
On Error: Retry
Max Tries: 3
Wait Between Tries: 10,000ms
This handles transient failures — LLM API rate limits, brief network issues, n8n execution queue congestion.
Workflow-Level Error Handler
Create a separate workflow named Pipeline Error Handler with an Error Trigger node as its trigger.
Log the failure to a database (Postgres or Supabase node works well):
const err = $trigger.error;
const exec = $trigger.execution;
return [{
json: {
error_type: err.name,
error_message: err.message,
node_that_failed: err.node?.name || 'unknown',
execution_id: exec.id,
execution_url: exec.url,
occurred_at: new Date().toISOString()
}
}];
Set this as your Error Workflow in Workflow Settings → Error Workflow of the orchestrator.
For high-stakes pipelines, add a Send Email or Slack node in the error handler so failures reach your inbox immediately.
Step 8: Testing End-to-End
Use n8n's Test Webhook URL (visible in the Webhook node) for development testing:
curl -X POST \
"https://your-n8n.domain/webhook-test/content-pipeline" \
-H "Content-Type: application/json" \
-H "X-Pipeline-Secret: your-secret-here" \
-d '{
"task": {
"title": "n8n vs Make.com for AI Workflow Automation in 2026",
"type": "comparison_post",
"target_audience": "developers evaluating automation platforms",
"tone": "technical",
"word_count": 1500,
"requirements": [
"Compare pricing at scale",
"Cover AI node capabilities in each platform",
"Include self-hosting comparison"
]
}
}'
Watch the execution in n8n's canvas. Each node turns green as it completes. Click any node to see exactly what it received and returned.
Debugging table — common issues:
| Symptom | Cause | Fix |
|---|---|---|
| Research Agent returns non-JSON | Temperature too high or model too small | Set temperature: 0, use gpt-4o-mini minimum |
| Sub-workflow not found | Sub-workflow not activated | Toggle Inactive → Active on both sub-workflows |
| Writer produces truncated content | Max tokens limit hit | Increase maxTokens on the OpenAI Chat Model node |
| Brave Search returns empty results | freshness: pm too restrictive for niche topic |
Remove freshness filter for niche/technical topics |
| Execute Sub-Workflow times out | Sub-workflow takes too long | Increase workflow timeout in Settings → Workflow Settings |
$('Node Name') reference fails |
Node name changed or misspelled | Check exact node name in the canvas |
Production Considerations
Self-Hosted vs. n8n Cloud
| Factor | Self-Hosted | n8n Cloud |
|---|---|---|
| Execution cost | Fixed (server cost only) | Per execution (Starter: 2,500/month included) |
| Data privacy | Full control | n8n sees execution data |
| Sub-workflow support | Full | Full |
| Queue mode (parallel) | Requires Redis + worker config | Handled automatically |
| Maintenance overhead | You own it | Managed |
| Cold start time | None | None |
For AI pipelines processing proprietary client data or content from internal knowledge bases, self-host. For prototypes and low-volume tools, n8n Cloud is significantly faster to get running.
We run this exact pipeline self-hosted on a $24/month DigitalOcean droplet (2 vCPU, 4GB RAM) running n8n via Docker Compose with PostgreSQL. It handles 200+ article pipeline runs per month with comfortable headroom.
# docker-compose.yml (abbreviated)
version: '3.8'
services:
n8n:
image: n8nio/n8n:latest
ports:
- "5678:5678"
environment:
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_DATABASE=n8n
- N8N_ENCRYPTION_KEY=your-random-key-here
- EXECUTIONS_MODE=queue
- QUEUE_BULL_REDIS_HOST=redis
volumes:
- n8n_data:/home/node/.n8n
postgres:
image: postgres:15
environment:
POSTGRES_DB: n8n
POSTGRES_USER: n8n
POSTGRES_PASSWORD: your-password-here
redis:
image: redis:7-alpine
Rate Limiting and Concurrency
If you run more than ~5 concurrent pipeline executions, you'll hit OpenAI's RPM limits before you hit n8n limits. Add a Wait node (2 seconds) between the Research Agent and Writer Agent calls to prevent rate limit errors from cascading through your pipeline.
For sustained high throughput, implement exponential backoff in a Code node:
// Exponential backoff before calling next agent
const attempt = parseInt($executionData?.retryAttempt) || 0;
const waitMs = Math.min(1000 * Math.pow(2, attempt), 32000);
// Return wait duration — connect to a Wait node
return [{ json: { wait_ms: waitMs, attempt } }];
Auditability — Store Every Execution
Add a Postgres node at the end of the orchestrator workflow to log every pipeline run:
INSERT INTO pipeline_executions (
request_id, title, content_type, word_count,
research_confidence, insufficiency_flags,
total_cost_usd, processing_time_ms, created_at
) VALUES (
'{{ $json.request_id }}',
'{{ $json.title }}',
'{{ $json.metadata.content_type }}',
{{ $json.word_count }},
'{{ $json.metadata.research_confidence }}',
{{ $json.insufficiency_flags }},
0.071,
{{ Date.now() - $('Parse Input').first().json.received_at_ms }},
NOW()
);
Clients want to see this table. "847 articles processed last month, average cost $0.071/article, average 68 seconds from request to draft" is a concrete deliverable you can put in a quarterly review.
Where n8n Shines for Multi-Agent Work
Visual execution debugging is genuinely excellent. When your Research Agent returns malformed JSON, you can click the node and see exactly what it produced — no log tailing, no printf debugging, no stack traces to parse. This alone saves multiple hours during development and maintenance.
Sub-workflow architecture enforces agent separation at the infrastructure level. It's not just conceptual — sub-workflows have their own execution logs, their own error workflows, and can be tested and updated independently. When the Research Agent needs a new tool, you add it to that sub-workflow without touching the orchestrator.
Self-hosting gives you complete data control — a meaningful differentiator for clients in regulated industries or those processing proprietary intellectual property. We've deployed this pipeline for clients who explicitly cannot route their content through third-party SaaS infrastructure.
Webhook triggers make agent systems feel like proper microservices. The orchestrator has an API endpoint. Any system — a CMS, a Slack bot, a mobile app — can trigger the pipeline with a standard HTTP POST. No polling, no SDK integration, no custom client code.
Model switching is one node change. Swap OpenAI Chat Model for Anthropic Chat Model and you're running Claude. Swap for an Ollama node and you're running a local model. No code changes, no workflow restructuring.
As an AI automation partner building these pipelines for clients in India, UAE, and Singapore, the visual debugging capability alone has cut our QA time on new pipeline builds by roughly 40% compared to Python-based equivalents.
Where n8n Still Struggles
No native cross-execution agent memory. Every execution starts fresh unless you've explicitly wired up Redis or Postgres memory nodes. For conversational multi-agent systems that need to remember prior interactions, this is a real gap. LangGraph and CrewAI handle cross-execution state more elegantly at the framework level. The workarounds work — but they add infrastructure and configuration overhead.
Context passing between sub-workflows is explicit and error-prone. There's no shared context store. You must pass every piece of data the Writer Agent needs from the orchestrator through the input JSON explicitly. If you add a new contextual field six months later, you have to find every place it needs to be plumbed through. We've had bugs in production because a new locale field was added to the task spec but not plumbed into the sub-workflow input.
Error surfacing from sub-workflows is opaque. When a sub-workflow fails, the parent workflow sees a generic Sub-workflow returned an error message. Finding which node inside the sub-workflow failed requires opening the sub-workflow execution log in a separate browser tab. For complex failures, this is genuinely tedious.
The AI Agent node has a reliability ceiling with smaller models. GPT-4o-mini handles straightforward tool calls reliably. It struggles when tool schemas have more than 3–4 parameters or when multi-step tool use decisions are required. If you're cost-optimising aggressively, test your specific prompts with the smaller model before committing to it — some tasks simply require the larger model's reasoning capacity.
Token usage is not natively tracked. n8n shows execution time but not token consumption per run. To build cost dashboards, you need to extract token usage from the OpenAI Chat Model node's output metadata ($json._response?.usage) and log it manually. There's no built-in cost monitoring. Build it yourself from day one — retrofitting observability into a running production pipeline is significantly more painful.
What This Architecture Looks Like in Our Client Work
We deployed this exact three-agent architecture — with some modifications — for a managed services client who publishes 20 articles per week across four industry verticals. The pipeline runs on a self-hosted n8n instance, triggered by a Directus CMS webhook when an editor marks an article brief as "ready for AI draft."
Research runs in 25–35 seconds. Writing runs in 40–60 seconds. Full pipeline: under 90 seconds from webhook trigger to formatted draft appearing in the editor's CMS queue.
The editor reviews, edits, adds firsthand perspective, and publishes. What previously took 3–4 hours of research plus writing per article now takes 20–30 minutes of editing and QA. Over 20 articles per week, that's 50–60 hours of work returned to the team every week — without adding headcount.
If you're building something similar or want this pipeline implemented and maintained for your team, see our AI automation services. For ongoing pipeline management with SLA-backed support, our managed services model handles the infrastructure, iteration, and monitoring.
FAQ
What version of n8n is required for this tutorial?
You need n8n 1.40+ for the AI Agent Tool node and Execute Sub-Workflow node in its current form. The workflow was written and tested against n8n 1.58. If you're on an older self-hosted instance, update with docker pull n8nio/n8n:latest and restart your container.
Can I use Claude instead of GPT-4o? Yes — swap OpenAI Chat Model nodes for Anthropic Chat Model nodes. Claude 3.5 Sonnet performs comparably to GPT-4o on writing tasks in our testing. Claude 3.5 Haiku is an excellent alternative to GPT-4o-mini for the Research Agent — it's more reliable at returning clean structured JSON output on the first attempt.
How do I handle Research Agent failures gracefully?
The IF node after the Research Agent call routes failures to a separate error path. In production, we recommend one automatic retry (re-call the sub-workflow with a prompt addendum: "Ensure you return ONLY valid JSON matching the exact schema"). If the second attempt also fails, return a structured error to the webhook caller and log it for manual review.
Can sub-workflows call other sub-workflows (nested agents)? Yes. n8n supports multiple levels of sub-workflow nesting. We've built pipelines with 3 levels (orchestrator → specialist → utility agent). Keep it shallow — 2 levels is comfortable to debug, 3 levels makes execution logs difficult to follow and error attribution becomes guesswork.
How do I prevent the Research Agent from running 10 tool-use iterations and burning tokens?
Set maxIterations to 5–6 on the AI Agent node. Also add explicit termination language in the system prompt: "Do not perform more than 3 search queries. Once you have sufficient information to complete the output schema, return immediately."
The Brave Search free tier gives 2,000 queries/month. Does this run out quickly? A single research run executes 2–4 queries. At 2,000 free queries, that's 500–1,000 research runs per month before you hit the paid tier. Brave Search Pro is $3/month for unlimited API calls — just use it in production.
How does this compare to building the same system in LangGraph or CrewAI? LangGraph gives you finer control over agent state, conditional graph execution, and complex routing logic. It's the right choice for systems with non-linear agent coordination, dynamic agent selection, or complex state management. n8n is faster to prototype, easier to debug visually, and easier for non-developers to maintain and modify. For client-facing pipelines where the client's team needs to update prompts or add tools without engineering support, n8n wins. For internal developer tooling where code-level control matters, LangGraph. We've shipped both in production — the choice is determined by who maintains it, not which is technically superior.
What's the cleanest way to version and iterate on system prompts without modifying workflow nodes?
Store all system prompts in a database table (we use a Directus collection). At the start of each sub-workflow, fetch the current active prompt via an HTTP Request node. This lets you update prompts without touching workflow configuration — even non-engineers can iterate on prompts through the CMS. Add a version field to the prompt record and log which version was used in each pipeline execution for rollback capability.
Rishabh Sethia, Founder & CEO of Innovatrix Infotech. Former SSE/Head of Engineering. DPIIT Recognized Startup. We build and maintain AI automation pipelines — from proof-of-concept to production — for D2C brands, agencies, and enterprise clients across India, UAE, and Singapore. AI Automation Services · Managed Services