If you've tried to automate SEO keyword research and content generation, you already know the problem: one AI call gives you a blog post that reads fine but ranks nowhere. What you actually need is a pipeline where one agent researches keywords, another writes around them, and a third checks whether the output is actually optimized before it ever touches your CMS.
That's exactly what we're building here. By the end of this tutorial, you'll have a working Python pipeline that uses three Claude agents connected through a tool-use orchestrator to go from a topic idea to a fully optimized, structured blog post ready to publish.
This is the same architecture we use at Naples AI to power SEO content generation for local businesses across Southwest Florida — and it works in production.
What You'll Build
You'll build a multi-agent SEO content pipeline using the Anthropic SDK and Python. The pipeline takes a seed topic, runs it through a keyword research agent, passes winning keywords to a content writer agent, and then feeds the draft to an SEO optimizer agent that returns a structured, publish-ready result.
The whole system is orchestrated by a single class that routes tool calls between agents automatically. No manual hand-off, no copy-pasting between prompts.
Prerequisites
- Python 3.10 or higher installed
- An Anthropic API key (get one at console.anthropic.com)
- Basic familiarity with Python classes and functions
anthropicSDK installed:pip install anthropic- A
.envfile or environment variable set forANTHROPIC_API_KEY - Optional:
python-dotenvfor loading your key from a file
Step 1: Set Up Your Claude API Environment
Start by installing the dependencies and confirming your API key works. This step catches auth issues early so they don't show up buried inside a multi-agent loop later.
setup.py
import os
import anthropic
from dotenv import load_dotenv
# Load API key from .env file if present
load_dotenv()
def verify_connection() -> bool:
"""Send a minimal test message to confirm the API key is valid."""
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=32,
messages=[{"role": "user", "content": "Reply with: connected"}]
)
reply = response.content[0].text.strip().lower()
return "connected" in reply
if __name__ == "__main__":
if verify_connection():
print("✓ Claude API connected successfully")
else:
print("✗ Connection failed — check your ANTHROPIC_API_KEY")
Run this with python setup.py before moving forward. You should see:
✓ Claude API connected successfully
If you get an AuthenticationError, double-check that your .env file contains ANTHROPIC_API_KEY=sk-ant-... with no trailing spaces or quotes around the value.
Step 2: Create the Keyword Research Agent
The keyword research agent takes a seed topic and returns a structured list of target keywords, search intent labels, and estimated competition levels. We define this as a tool so the orchestrator can call it cleanly and parse the output without string hacking.
Notice we're not actually hitting a third-party keyword API here — Claude generates realistic keyword research based on its training data, which is surprisingly useful for content planning. You can swap in a real API (like DataForSEO) as a tool return value later if you want live volume data.
keyword_agent.py
import os
import json
import anthropic
# Tool definition that the orchestrator will register
KEYWORD_RESEARCH_TOOL = {
"name": "keyword_research",
"description": (
"Generates a list of SEO keyword opportunities for a given topic. "
"Returns primary keywords, long-tail variants, search intent, and "
"estimated competition level (low/medium/high)."
),
"input_schema": {
"type": "object",
"properties": {
"topic": {
"type": "string",
"description": "The seed topic or niche to research keywords for."
},
"target_audience": {
"type": "string",
"description": "Who the content is written for (e.g., 'small business owners in Florida')."
},
"num_keywords": {
"type": "integer",
"description": "How many keyword ideas to return. Default is 8.",
"default": 8
}
},
"required": ["topic", "target_audience"]
}
}
def run_keyword_research(topic: str, target_audience: str, num_keywords: int = 8) -> dict:
"""
Calls Claude directly to produce structured keyword research output.
Returns a dict with a 'keywords' list, each containing keyword data.
"""
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
system_prompt = (
"You are an expert SEO strategist. When given a topic and audience, "
"return a JSON object with a 'keywords' array. Each keyword object must include: "
"'keyword' (string), 'intent' (informational/commercial/transactional/navigational), "
"'competition' (low/medium/high), 'monthly_searches_estimate' (integer), "
"and 'content_angle' (a one-sentence content idea for that keyword). "
"Return ONLY valid JSON. No markdown, no explanation."
)
user_message = (
f"Topic: {topic}\n"
f"Target audience: {target_audience}\n"
f"Return {num_keywords} keyword opportunities."
)
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=1024,
system=system_prompt,
messages=[{"role": "user", "content": user_message}]
)
raw = response.content[0].text.strip()
# Parse and validate the JSON response
keyword_data = json.loads(raw)
return keyword_data
if __name__ == "__main__":
results = run_keyword_research(
topic="AI automation for restaurants",
target_audience="restaurant owners in Southwest Florida",
num_keywords=4
)
print(json.dumps(results, indent=2))
{
"keywords": [
{
"keyword": "restaurant AI ordering system",
"intent": "commercial",
"competition": "low",
"monthly_searches_estimate": 1200,
"content_angle": "How AI-powered ordering systems cut wait times by 40% in busy restaurants."
},
{
"keyword": "automate restaurant inventory management",
"intent": "informational",
"competition": "medium",
"monthly_searches_estimate": 880,
"content_angle": "Step-by-step guide to replacing manual inventory sheets with AI tools."
},
{
"keyword": "AI chatbot for restaurant reservations",
"intent": "commercial",
"competition": "low",
"monthly_searches_estimate": 640,
"content_angle": "Why Naples restaurants are switching from phone reservations to AI chatbots."
},
{
"keyword": "reduce food waste with AI",
"intent": "informational",
"competition": "medium",
"monthly_searches_estimate": 2100,
"content_angle": "How predictive AI forecasting helps restaurants order smarter and waste less."
}
]
}
Step 3: Build the Content Writer Agent
The content writer agent receives the keyword list from Step 2 and produces a full draft blog post. We give it a detailed system prompt so it naturally weaves in the primary keyword, uses the long-tail variants in subheadings, and writes in a voice that matches the brand.
The prompt engineering here does the heavy lifting. You're not asking Claude to "write a blog post" — you're giving it SEO intent, structure rules, and word count targets so it comes back with something you can actually use.
content_writer_agent.py
import os
import anthropic
from typing import Optional
# Tool definition for the orchestrator
CONTENT_WRITER_TOOL = {
"name": "write_content",
"description": (
"Writes a complete SEO-optimized blog post draft based on a primary keyword, "
"supporting keywords, target audience, and desired word count. "
"Returns the full post as a structured string with H1, H2s, and body text."
),
"input_schema": {
"type": "object",
"properties": {
"primary_keyword": {
"type": "string",
"description": "The main keyword to optimize the post for."
},
"supporting_keywords": {
"type": "array",
"items": {"type": "string"},
"description": "Secondary keywords to include naturally in the post."
},
"content_angle": {
"type": "string",
"description": "The specific content angle or hook for the article."
},
"target_audience": {
"type": "string",
"description": "Who this post is written for."
},
"word_count": {
"type": "integer",
"description": "Approximate target word count. Default is 1200.",
"default": 1200
},
"brand_voice": {
"type": "string",
"description": "Description of the writing tone (e.g., 'conversational, expert, no jargon').",
"default": "conversational and expert"
}
},
"required": ["primary_keyword", "supporting_keywords", "content_angle", "target_audience"]
}
}
def run_content_writer(
primary_keyword: str,
supporting_keywords: list[str],
content_angle: str,
target_audience: str,
word_count: int = 1200,
brand_voice: str = "conversational and expert",
custom_instructions: Optional[str] = None
) -> str:
"""
Uses Claude to write a full blog post draft optimized around the given keywords.
Returns the raw post text as a string.
"""
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
supporting_kw_str = ", ".join(supporting_keywords)
system_prompt = (
f"You are an expert content writer specializing in SEO blog posts. "
f"Write in a {brand_voice} tone. "
f"Structure every post with: one H1 containing the primary keyword, "
f"at least 4 H2 subheadings (use supporting keywords naturally in at least 2 of them), "
f"short paragraphs of 2-3 sentences, and a clear conclusion. "
f"Do NOT use the phrase 'In conclusion'. "
f"Aim for approximately {word_count} words. "
f"Write for {target_audience}. "
f"Return only the blog post text — no metadata, no commentary."
)
user_message = (
f"Primary keyword: {primary_keyword}\n"
f"Supporting keywords: {supporting_kw_str}\n"
f"Content angle: {content_angle}\n"
)
if custom_instructions:
user_message += f"\nAdditional instructions: {custom_instructions}"
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=4096,
system=system_prompt,
messages=[{"role": "user", "content": user_message}]
)
return response.content[0].text.strip()
if __name__ == "__main__":
draft = run_content_writer(
primary_keyword="restaurant AI ordering system",
supporting_keywords=["automate restaurant orders", "AI for restaurants", "reduce order errors"],
content_angle="How AI-powered ordering systems cut wait times by 40% in busy restaurants.",
target_audience="restaurant owners in Southwest Florida",
word_count=600 # Shorter for demo purposes
)
# Print just the first 500 characters to preview
print(draft[:500])
print("\n... [full draft continues] ...")
# How a Restaurant AI Ordering System Can Cut Your Wait Times by 40%
If your kitchen is backed up and your front-of-house staff is juggling tablets,
paper tickets, and verbal orders all at once, you already know something has to change.
A restaurant AI ordering system doesn't just digitize the process — it eliminates the
bottlenecks that slow everything down during your busiest hours.
## What a Restaurant AI Ordering System Actually Does
Most restaurant owners picture a kiosk when they hear "AI ordering." The reality is
more powerful than that. Modern systems learn your peak hours, flag order errors before
they hit the kitchen, and route tickets automatically based on station availability.
## How to Automate Restaurant Orders Without Disrupting Your Staff
... [full draft continues] ...
Step 4: Implement the SEO Optimizer Agent
The SEO optimizer reads the draft from Step 3 and returns a structured report: a final optimized version of the post, a meta description, a focus keyword density score, and a list of SEO issues found. We use Claude's structured output capability here to make sure the response is always parseable.
This agent is the quality gate. It catches thin content, missing keyword usage, and weak title tags before anything goes out the door.
seo_optimizer_agent.py
import os
import json
import anthropic
# Tool definition for the orchestrator
SEO_OPTIMIZER_TOOL = {
"name": "optimize_seo",
"description": (
"Analyzes a blog post draft and returns an SEO-optimized version along with "
"a structured quality report. Checks keyword density, meta description quality, "
"title tag optimization, internal link suggestions, and content gaps."
),
"input_schema": {
"type": "object",
"properties": {
"draft_content": {
"type": "string",
"description": "The full blog post draft text to analyze and optimize."
},
"primary_keyword": {
"type": "string",
"description": "The main keyword the post should rank for."
},
"target_url_slug": {
"type": "string",
"description": "The intended URL slug for this post (e.g., 'restaurant-ai-ordering-system')."
}
},
"required": ["draft_content", "primary_keyword", "target_url_slug"]
}
}
def run_seo_optimizer(
draft_content: str,
primary_keyword: str,
target_url_slug: str
) -> dict:
"""
Passes the draft to Claude for SEO analysis and optimization.
Returns a structured dict with the optimized post and a quality report.
"""
client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
system_prompt = (
"You are a senior SEO editor. Analyze the provided blog post and return a JSON object "
"with these exact keys: "
"'optimized_content' (the full improved post as a string), "
"'meta_description' (150-160 characters, includes primary keyword), "
"'title_tag' (50-60 characters, keyword-first format), "
"'url_slug' (confirmed or corrected slug), "
"'keyword_density_percent' (float, e.g. 1.4), "
"'word_count' (integer), "
"'seo_issues_found' (array of strings describing problems you fixed), "
"'internal_link_suggestions' (array of strings: topics that should link to this post), "
"'seo_score' (integer 0-100 representing overall on-page SEO quality). "
"Return ONLY valid JSON. No markdown fences, no commentary."
)
user_message = (
f"Primary keyword: {primary_keyword}\n"
f"Target URL slug: {target_url_slug}\n\n"
f"Draft content:\n{draft_content}"
)
response = client.messages.create(
model="claude-sonnet-4-5",
max_tokens=8096,
system=system_prompt,
messages=[{"role": "user", "content": user_message}]
)
raw = response.content[0].text.strip()
# Parse the structured JSON response
result = json.loads(raw)
return result
if __name__ == "__main__":
# Quick test with a minimal draft
sample_draft = """
# AI Ordering for Restaurants
Restaurants are starting to use AI. It helps with orders. Many places in Florida
are doing this now. You should think about it for your business.
## Why It Matters
Order errors cost money. AI reduces them.
"""
result = run_seo_optimizer(
draft_content=sample_draft,
primary_keyword="restaurant AI ordering system",
target_url_slug="restaurant-ai-ordering-system"
)
# Print the report without the full content body to keep output readable
summary = {k: v for k, v in result.items() if k != "optimized_content"}
print(json.dumps(summary, indent=2))
{
"meta_description": "Learn how a restaurant AI ordering system reduces errors, cuts wait times by 40%, and helps Southwest Florida restaurants run smoother during peak hours.",
"title_tag": "Restaurant AI Ordering System: Cut Wait Times by 40%",
"url_slug": "restaurant-ai-ordering-system",
"keyword_density_percent": 1.6,
"word_count": 1247,
"seo_issues_found": [
"Primary keyword missing from first 100 words — added to opening paragraph",
"H1 was too generic — updated to include primary keyword in keyword-first format",
"No internal link anchors present — added suggestion placeholders",
"Meta description was absent — generated from content"
],
"internal_link_suggestions": [
"AI chatbot for restaurant reservations",
"how to reduce food waste with AI",
"automate restaurant inventory management"
],
"seo_score": 84
}
Step 5: Connect Agents with Tool Use — The Orchestrator
Here's where everything comes together. The orchestrator class registers all three tools, takes a single topic input, and runs the full pipeline automatically. It handles tool routing, passes outputs between agents, and returns the final publish-ready result.
This is the pattern we use for all multi-agent AI systems at Naples AI. One orchestrator, clean tool definitions, deterministic routing. It's easier to debug than a chain of raw API calls and easier to extend than you'd think.
orchestrator.py
import os
import json
import anthropic
from dotenv import load_dotenv
from keyword_agent import run_keyword_research, KEYWORD_RESEARCH_TOOL
from content_writer_agent import run_content_writer, CONTENT_WRITER_TOOL
from seo_optimizer_agent import run_seo_optimizer, SEO_OPTIMIZER_TOOL
load_dotenv()
class SEOContentOrchestrator:
"""
Orchestrates the full multi-agent SEO content pipeline.
Takes a topic + audience and returns a publish-ready blog post
with full SEO metadata and quality scores.
"""
def __init__(self):
self.client = anthropic.Anthropic(api_key=os.environ.get("ANTHROPIC_API_KEY"))
self.model = "claude-sonnet-4-5"
# Register all available tools for the orchestration layer
self.tools = [
KEYWORD_RESEARCH_TOOL,
CONTENT_WRITER_TOOL,
SEO_OPTIMIZER_TOOL
]
# Map tool names to their handler functions
self.tool_handlers = {
"keyword_research": self._handle_keyword_research,
"write_content": self._handle_write_content,
"optimize_seo": self._handle_optimize_seo
}
# Shared state that persists across agent steps
self.pipeline_state = {
"keywords": None,
"primary_keyword": None,
"draft": None,
"final_result": None
}
def _handle_keyword_research(self, tool_input: dict) -> str:
"""Runs keyword research and stores results in pipeline state."""
result = run_keyword_research(
topic=tool_input["topic"],
target_audience=tool_input["target_audience"],
num_keywords=tool_input.get("num_keywords", 8)
)
#