Key Takeaways
- Single-agent AI systems often struggle with complex content creation; a multi-agent approach using CrewAI allows for specialized roles (Researcher, Writer, Editor) that mimic a real editorial team for higher-quality output.
- This tutorial walks through building a four-agent crew that automates the entire blog post creation process, from initial research to a final, polished draft.
- The final agent, the Publisher, uses a custom tool to programmatically publish the finished article to a headless CMS via its REST API, eliminating manual copy-pasting.
- By connecting your CrewAI pipeline to a blog-focused CMS like Wisp, you can fully automate your content workflow from idea to live post.
You've tried the single-agent approach. You give an AI a topic, it spits out a 1,000-word post, and the result reads like every other AI-generated article on the internet. The research is shallow, the structure is generic, and you've blended three different writing styles into one incoherent draft. The same problem surfaces in content: one agent juggling research, writing, editing, and publishing simply can't hold context across all four disciplines.
The fix is specialization. A multi-agent system with CrewAI lets you define a crew of focused agents, each with a clear role, handing output from one stage to the next like a real editorial team. This tutorial walks through building a four-agent blog writing crew: Researcher, Writer, Editor, and Publisher, where the final step pushes the finished post directly to Wisp, a headless CMS built for blogs on Next.js and React.
Let's dig into the architecture, the code, and the Wisp Content Management System (CMS) integration that ties it all together.
The CrewAI Content Team Architecture
The crew runs sequentially. Each agent receives the previous agent's output as context, creating a progressive refinement pipeline from raw research to a published post. No single agent tries to do everything, which keeps outputs focused and reduces the contextual drift that plagues single-agent content workflows.
Here are the four agents and their responsibilities:
- Researcher. Gathers facts, recent data, and credible sources on a given topic using web search tools. Outputs a structured research brief and outline.
- Writer. Drafts the full blog post from the research brief. Focused entirely on narrative structure and readability.
- Editor. Reviews the draft for clarity, brand voice, grammar, and basic Search Engine Optimization (SEO) structure. Acts as the quality gate before publication.
- Publisher. Takes the polished post, maps it to Wisp's Application Programming Interface (API) schema, and posts it via the REST API.
This division of labour is what separates a producible content pipeline from a one-shot prompt.
Prerequisites for Building Your Crew
Before writing any code, make sure you have the following in place:
- Python: CrewAI requires version
>=3.10and<3.14. - CrewAI installed: Run
uv pip install crewai crewai-toolsto get the framework and its bundled tools. - LLM API key: An Application Programming Interface key from OpenAI, Anthropic, or another supported provider. Set it as
OPENAI_API_KEYin your environment. - Serper API key: A web search key from Serper to give the Researcher agent real-time search capability. Set it as
SERPER_API_KEY. - Wisp API credentials: Your
WISP_BLOG_ID(found in your Wisp setup page) and aWISP_API_TOKENgenerated from your account settings. The REST API docs cover token creation in detail.
Step 1: Setting Up Your CrewAI Project
Scaffold the project using the CrewAI command-line interface (CLI):
crewai create crew blog_writing_team
cd blog_writing_team
This generates a structured project with the following layout:
blog_writing_team/
├── src/
│ └── blog_writing_team/
│ ├── config/
│ │ ├── agents.yaml
│ │ └── tasks.yaml
│ ├── tools/
│ │ └── wisp_tool.py # We'll create this
│ └── crew.py
├── .env
└── pyproject.toml
Add your environment variables to .env:
OPENAI_API_KEY=your_key_here
SERPER_API_KEY=your_key_here
WISP_BLOG_ID=your_blog_id_here
WISP_API_TOKEN=your_token_here
Step 2: Defining Agents and Tasks
Open config/agents.yaml and define the four agents:
researcher:
role: 'Expert Technology Researcher'
goal: 'Find the most relevant and up-to-date information on a given topic.'
backstory: 'An expert researcher with a knack for digging up credible sources and synthesizing complex information into a clear, actionable brief.'
tools:
- 'SerperDevTool'
allow_delegation: false
writer:
role: 'Senior Content Writer'
goal: 'Write a compelling, well-structured blog post from a research brief.'
backstory: 'A professional writer with years of experience in tech blogging, skilled at turning technical details into engaging narratives.'
allow_delegation: false
editor:
role: 'Meticulous Content Editor'
goal: 'Review and refine a blog post draft for clarity, tone, and grammar.'
backstory: 'An experienced editor with a sharp eye for detail and a deep understanding of brand voice and content strategy.'
allow_delegation: false
publisher:
role: 'CMS Integration Specialist'
goal: 'Take a final, edited blog post and publish it to the Wisp CMS via its API.'
backstory: 'A technical specialist who understands CMS APIs and formats content correctly before publishing.'
allow_delegation: false
Now open config/tasks.yaml. The context field is what chains the tasks together. Each agent receives the previous task's output as its starting point:
research_task:
description: >
Conduct comprehensive research on {topic}. Identify key facts, recent trends,
and supporting data points. Compile everything into a detailed research brief.
expected_output: >
A research brief with an outline, key points, and a list of source URLs.
agent: researcher
writing_task:
description: >
Using the research brief, write a full blog post on {topic}. The post
should be engaging, informative, and at least 1200 words long.
expected_output: >
The complete draft of the blog post in markdown format.
agent: writer
context:
- research_task
editing_task:
description: >
Review the draft blog post for clarity, grammar, and style. Check that it
matches a professional yet conversational brand voice. Provide the final version
with a suggested title and a 160-character meta description on separate lines.
expected_output: >
The final polished blog post in markdown, with Title: and Meta Description:
lines at the top, followed by Body: and the full post content.
agent: editor
context:
- writing_task
publishing_task:
description: >
Take the final blog post and publish it to the Wisp CMS using the
WispPublisherTool. Extract the title, meta description, and body from
the editor's output. Confirm successful publication.
expected_output: >
A confirmation message with the published post's URL on Wisp.
agent: publisher
context:
- editing_task
Step 3: Orchestrating the Crew in Python
With agents and tasks configured, wire everything together in crew.py:
from crewai import Agent, Crew, Process, Task
from crewai.project import CrewBase, agent, crew, task
from crewai_tools import SerperDevTool
from tools.wisp_tool import WispPublisherTool
@CrewBase
class BlogWritingTeam():
agents_config = 'config/agents.yaml'
tasks_config = 'config/tasks.yaml'
@agent
def researcher(self) -> Agent:
return Agent(config=self.agents_config['researcher'], tools=[SerperDevTool()])
@agent
def writer(self) -> Agent:
return Agent(config=self.agents_config['writer'])
@agent
def editor(self) -> Agent:
return Agent(config=self.agents_config['editor'])
@agent
def publisher(self) -> Agent:
return Agent(config=self.agents_config['publisher'], tools=[WispPublisherTool()])
@task
def research_task(self) -> Task:
return Task(config=self.tasks_config['research_task'])
@task
def writing_task(self) -> Task:
return Task(config=self.tasks_config['writing_task'])
@task
def editing_task(self) -> Task:
return Task(config=self.tasks_config['editing_task'])
@task
def publishing_task(self) -> Task:
return Task(config=self.tasks_config['publishing_task'])
@crew
def crew(self) -> Crew:
return Crew(
agents=self.agents,
tasks=self.tasks,
process=Process.sequential,
verbose=True
)
To kick things off, run:
# main.py
from blog_writing_team.crew import BlogWritingTeam
result = BlogWritingTeam().crew().kickoff(
inputs={'topic': 'The Rise of Multi-Agent AI Systems in 2025'}
)
print(result)
Step 4: The Publisher Agent and the Wisp CMS Integration
This is where the crewai blog writing agent CMS integration comes together. The Publisher agent needs a custom tool that maps the Editor's structured output to Wisp's REST API schema. Create tools/wisp_tool.py:
import os
import re
import requests
from crewai.tools import BaseTool
from pydantic import BaseModel, Field
def parse_post_from_output(output: str) -> dict:
title_match = re.search(r"^Title:\s*(.+)$", output, re.MULTILINE)
desc_match = re.search(r"^Meta Description:\s*(.+)$", output, re.MULTILINE)
body_match = re.search(r"^Body:\s*\n(.*)", output, re.DOTALL)
return {
"title": title_match.group(1).strip() if title_match else "Untitled Post",
"description": desc_match.group(1).strip() if desc_match else "",
"body": body_match.group(1).strip() if body_match else output,
}
class WispPublishSchema(BaseModel):
final_post_content: str = Field(
description="The complete final blog post including Title, Meta Description, and Body."
)
class WispPublisherTool(BaseTool):
name: str = "Wisp Publisher Tool"
description: str = "Publishes a formatted blog post to the Wisp CMS via the REST API."
args_schema: type[BaseModel] = WispPublishSchema
def _run(self, final_post_content: str) -> str:
blog_id = os.getenv("WISP_BLOG_ID")
api_token = os.getenv("WISP_API_TOKEN")
if not blog_id or not api_token:
return "Error: WISP_BLOG_ID and WISP_API_TOKEN must be set as environment variables."
post_data = parse_post_from_output(final_post_content)
# Wisp's REST API expects the body in TipTap JSON format.
# This wraps the markdown content in a single paragraph node.
# See the Wisp REST API docs for the full schema.
wisp_payload = {
"title": post_data["title"],
"description": post_data["description"],
"status": "draft", # Change to "published" when ready
"content": {
"type": "doc",
"content": [
{
"type": "paragraph",
"content": [{"type": "text", "text": post_data["body"]}]
}
]
}
}
headers = {
"Authorization": f"Bearer {api_token}",
"Content-Type": "application/json",
}
api_url = f"https://www.wisp.blog/api/v1/{blog_id}/posts"
try:
response = requests.post(api_url, headers=headers, json=wisp_payload)
response.raise_for_status()
slug = response.json().get("slug", "")
return f"Post published to Wisp. Slug: {slug}"
except requests.exceptions.RequestException as e:
return f"Error publishing to Wisp: {e}"
A few things worth noting here. First, Wisp's body field uses TipTap's JSON document format. The example wraps the markdown in a paragraph node. Check the Wisp REST API docs for the full schema if you want to preserve markdown heading structure during import. Second, the post is set to "draft" by default. Switch to "published" once you're confident in the pipeline's output quality.
Running and Triggering Your Crew
For a one-off run, execute python main.py from the project root. To pass the topic dynamically from the terminal:
# main.py with sys.argv support
import sys
from blog_writing_team.crew import BlogWritingTeam
topic = sys.argv[1] if len(sys.argv) > 1 else "AI agent frameworks in 2025"
BlogWritingTeam().crew().kickoff(inputs={'topic': topic})
Then call it with python main.py "Your topic here". For scheduled runs, a cron job works well for simple setups. For production pipelines, wrapping the crew in a FastAPI endpoint lets you trigger it from other applications, webhooks, or CI/CD events — a common pattern when scaling agentic workflows.
Production Tips for Your Content Crew
Shipping this to production introduces the same challenges that affect any distributed system. Developers who have built multi-agent systems at enterprise scale consistently flag three areas to address before going live.
Cost Management
Without limits, agents can enter planning loops that consume tokens without producing output. Set a max token budget per run using CrewAI's max_iter and max_execution_time parameters on each agent. The orchestrator should monitor consumption and halt if a threshold is crossed.
Error Handling and Checkpointing
Wrap all API calls in try-except blocks and log failures with the full task context. The real goal isn't preventing failures. It's recovering gracefully. Save each task's output to a local file or database after it completes, so a failure in the Publisher step doesn't require re-running the Researcher from scratch.
Logging and Observability
Set verbose=True during development to trace every agent decision. In production, pipe CrewAI's output to a structured logger. When the Publisher agent fails, you want the exact payload it tried to send, not just a generic exception.
Once content is live in Wisp, the pipeline gains additional value from Wisp's built-in features. Each published post can carry AI contextual CTAs that match to article content via embeddings, and related post suggestions that keep readers moving through your content library. Developers managing multiple client blogs can also take advantage of multi-tenancy support: a single Wisp account handles multiple blogs, each with separate content and settings.
Connect Your Crew to a Headless CMS
Building a multi-agent AI crew isn't just a novelty, it's a practical way to produce better content, faster. By assigning specialized roles, your AI team can handle everything from research to writing. Connecting that crew to a CMS with a solid API completes the loop, turning a manual copy-paste job into a fully automated pipeline.
Your next step? Start small. Try building a simple two-agent crew to see the handoff from a Researcher to a Writer in action.
Once your pipeline is running, the final bottleneck is often the CMS itself. Wisp is designed for this exact workflow, with a clean REST API that makes programmatic publishing simple. The free plan includes full API access, so you can explore the docs and connect your own CrewAI publisher without any commitment.
FAQs
What is a multi-agent AI system like CrewAI?
A multi-agent AI system like CrewAI is a framework that uses multiple specialized AI agents working together on a complex task. This approach improves quality by assigning specific roles, similar to a real-world team.
Why use multiple agents instead of one powerful AI?
Using multiple agents prevents context contamination and improves focus. A single AI struggles to manage distinct tasks like research and editing, while specialized agents excel at their roles for higher-quality output.
How does the Publisher agent work?
The Publisher agent works by using a custom tool to connect to a CMS via its REST API. It takes the final text, formats it into the required JSON schema, and programmatically sends it to publish the post.
Can I use a different CMS with this CrewAI setup?
Yes, you can use a different CMS with this CrewAI setup. You would need to modify the WispPublisherTool to match the API schema and authentication requirements of your chosen content management system.
What are the key requirements to build this content crew?
The key requirements to build this content crew are Python, the CrewAI library, and API keys for an LLM (like OpenAI), a search tool (like Serper), and your target CMS (like Wisp).
How do you handle errors or agent failures in the pipeline?
To handle errors, you should implement checkpointing by saving the output of each successful task. This allows you to resume the process from the last completed step instead of starting over if an agent fails.
How can I control the cost of running a CrewAI workflow?
You can control the cost of a CrewAI workflow by setting limits on each agent. Use parameters like max_iter and max_execution_time to prevent agents from entering expensive loops and monitor token usage.


