← Back to DevBytes

Building a Content Writing Agent with CrewAI: Complete Guide

What is CrewAI?

CrewAI is an open-source framework designed to orchestrate role-playing, autonomous AI agents that collaborate to accomplish complex tasks. Think of it as assembling a team of specialized professionals — each with a defined role, goal, and backstory — who work together, share insights, and build upon each other's outputs to deliver a polished final product.

At its core, CrewAI leverages large language models (LLMs) like GPT-4, Claude, or local models via providers such as Ollama, and structures them into crews — groups of agents with complementary skills. For content writing, this means you can create a dedicated team where one agent researches trending topics, another drafts compelling copy, a third edits for tone and grammar, and a fourth formats the final piece — all without manual handoffs.

The framework introduces several key abstractions:

Unlike simple prompt chaining, CrewAI agents engage in genuine collaboration. An agent can receive the output of a previous agent as context, ask clarifying questions (when configured), and iterate on feedback. This makes it ideal for content pipelines where raw research must be transformed into structured drafts, refined, and polished into publication-ready material.

Why CrewAI Matters for Content Writing

Content creation today demands speed, consistency, and quality at scale. A single LLM prompt often produces generic, surface-level text. CrewAI addresses this by breaking the content production process into specialized stages, each handled by an agent optimized for that particular phase. Here's why this matters:

Specialization Drives Quality

A single "write an article" prompt asks one model to research, structure, draft, and edit simultaneously. In practice, the model cuts corners — shallow research, repetitive phrasing, or weak transitions. CrewAI lets you assign a dedicated researcher agent who uses web search tools to gather fresh data, a writer agent who excels at narrative flow, and an editor agent who ruthlessly polishes grammar and tone. Each agent operates within its zone of competence, dramatically improving output quality.

Sequential Context Passing

CrewAI automatically passes the output of one task as context to the next. The writer receives the researcher's structured findings. The editor receives the writer's complete draft. This chain preserves institutional knowledge across the pipeline — no information is lost between steps. You can also configure tasks to run in parallel when appropriate, then merge results.

Tool Integration

Agents can be equipped with tools from CrewAI's built-in library or custom tools you define. A research agent might use SerperDevTool for Google search, WebsiteSearchTool for deep-diving into specific URLs, or ScrapeWebsiteTool to extract raw content. This grounds your content in real, current data rather than the model's training cutoff.

Scalable Content Operations

Once you define a crew, you can invoke it programmatically — generating dozens of articles with consistent structure, tone, and quality. This is transformative for SEO-driven content teams, newsletter writers, or documentation generators. You define the process once, then scale it across topics.

Human-in-the-Loop Refinement

CrewAI supports callback functions and iterative feedback loops. You can insert human review steps between tasks, approve drafts, or request revisions programmatically. This keeps you in control while automating the heavy lifting.

Setting Up Your Environment

Before building your content writing crew, install the necessary packages. CrewAI requires Python 3.10+ and works best in a virtual environment.

# Create and activate a virtual environment
python -m venv crewai-env
source crewai-env/bin/activate  # On Windows: crewai-env\Scripts\activate

# Install CrewAI with tools support
pip install crewai crewai-tools

You'll also need API keys for your chosen LLM provider and any tools you plan to use. Store these in a .env file or set them as environment variables:

# .env file
OPENAI_API_KEY=your-openai-api-key
SERPER_API_KEY=your-serper-api-key  # For Google search tool
ANTHROPIC_API_KEY=your-anthropic-key  # If using Claude

Load environment variables early in your script:

from dotenv import load_dotenv
load_dotenv()

Designing Your Content Writing Crew

A well-architected content writing crew typically involves three to five agents. Let's walk through a proven configuration:

For this tutorial, we'll build a core crew with Researcher, Writer, and Editor agents — a minimal viable pipeline that produces polished articles.

Defining Agents

Each agent is defined with a role, goal, backstory, and optional tools. The backstory is crucial — it shapes the agent's persona and influences its decision-making style. Write backstories that give agents a professional identity.

Research Agent

from crewai import Agent
from crewai_tools import SerperDevTool, WebsiteSearchTool

# Tools for the researcher
search_tool = SerperDevTool()
web_search_tool = WebsiteSearchTool()

researcher = Agent(
    role="Senior Research Analyst",
    goal="Conduct thorough, up-to-date research on assigned topics and compile structured findings",
    backstory=(
        "You are a veteran research analyst with 15 years of experience at a leading think tank. "
        "You excel at finding credible sources, extracting key statistics, identifying expert quotes, "
        "and organizing information into clear, logical briefs. You verify facts across multiple sources "
        "and always note the source of each key claim. You present findings in a structured format "
        "with sections for key statistics, expert perspectives, historical context, and current trends."
    ),
    tools=[search_tool, web_search_tool],
    verbose=True,
    allow_delegation=False,
    llm="gpt-4-turbo"  # Or "claude-3-opus-20240229" if using Anthropic
)

Content Writer Agent

writer = Agent(
    role="Senior Content Writer",
    goal="Craft engaging, well-structured articles from research briefs that captivate readers and convey information clearly",
    backstory=(
        "You are an award-winning content writer with a decade of experience writing for major publications "
        "like The Atlantic, Wired, and The New Yorker. Your writing style is clear, engaging, and accessible "
        "without sacrificing depth. You know how to hook readers with compelling openings, structure articles "
        "for maximum readability, and end with memorable conclusions. You adapt tone to the target audience "
        "and always cite sources provided in the research brief."
    ),
    verbose=True,
    allow_delegation=False,
    llm="gpt-4-turbo"
)

Editor Agent

editor = Agent(
    role="Chief Editor",
    goal="Ensure content meets the highest standards of accuracy, clarity, grammar, and brand voice consistency",
    backstory=(
        "You are the chief editor at a prestigious digital publication. You have a hawk-like eye for "
        "grammatical errors, logical gaps, and tonal inconsistencies. You enforce the publication's style guide "
        "religiously — active voice, no jargon without explanation, short paragraphs, and conversational yet "
        "authoritative tone. You provide specific, actionable revision notes and produce a final polished version. "
        "You never change the factual content without flagging the discrepancy against the research brief."
    ),
    verbose=True,
    allow_delegation=False,
    llm="gpt-4-turbo"
)

Defining Tasks

Tasks specify what each agent should accomplish, the expected output format, and which agent is responsible. Tasks can depend on each other through context passing — the output of one task becomes available to subsequent tasks.

Research Task

from crewai import Task

research_task = Task(
    description=(
        "Research the following topic comprehensively: {topic}\n\n"
        "Your research brief must include:\n"
        "1. A summary of the topic's importance and relevance (2-3 sentences)\n"
        "2. Five to seven key statistics with source attributions\n"
        "3. Three to four expert perspectives or notable quotes on the topic\n"
        "4. Current trends and recent developments (last 12 months)\n"
        "5. Historical context if relevant\n"
        "6. A list of 3-5 credible sources with URLs\n\n"
        "Use the search tools extensively. Verify claims across multiple sources. "
        "Output a structured research brief with clear section headers."
    ),
    agent=researcher,
    expected_output="A comprehensive research brief with all requested sections, properly sourced and organized",
    async_execution=False
)

Writing Task

writing_task = Task(
    description=(
        "Using the research brief provided as context, write a complete article on: {topic}\n\n"
        "Article requirements:\n"
        "- Target audience: {audience}\n"
        "- Tone: {tone}\n"
        "- Word count: approximately {word_count} words\n"
        "- Structure: compelling hook/introduction, well-organized body sections with clear subheadings, "
        "and a memorable conclusion with a call-to-action or thought-provoking closing\n"
        "- Incorporate the statistics and expert quotes from the research brief naturally\n"
        "- Cite sources inline where appropriate\n"
        "- Use short paragraphs (3-5 sentences) for readability\n"
        "- Write in active voice, avoid jargon without explanation\n\n"
        "The article should feel polished and publication-ready upon completion."
    ),
    agent=writer,
    expected_output="A complete, polished article draft in markdown format with proper headings, paragraphs, and source attributions",
    context=[research_task],  # Writer receives research output as context
    async_execution=False
)

Editing Task

editing_task = Task(
    description=(
        "Edit the article draft provided as context thoroughly. Your editing process:\n\n"
        "1. Verify all facts and statistics against the original research brief — flag any discrepancies\n"
        "2. Correct grammar, punctuation, and spelling errors\n"
        "3. Improve sentence structure for flow and readability\n"
        "4. Ensure consistent tone matching the target audience ({audience}) and desired tone ({tone})\n"
        "5. Check that paragraphs are concise (3-5 sentences each)\n"
        "6. Verify the hook is compelling and the conclusion is impactful\n"
        "7. Apply the house style guide: active voice, no unexplained jargon, conversational yet authoritative\n\n"
        "Output the final, publication-ready article in clean markdown format. "
        "Include a brief 'Editor's Note' section at the top summarizing the changes made."
    ),
    agent=editor,
    expected_output="Final publication-ready article in markdown with an editor's note summarizing revisions",
    context=[writing_task],  # Editor receives writer's draft as context
    async_execution=False
)

Assembling and Running the Crew

With agents and tasks defined, you assemble them into a crew and kick off the execution. The crew manages the sequential flow — research → writing → editing — automatically.

from crewai import Crew, Process

# Assemble the content writing crew
content_crew = Crew(
    agents=[researcher, writer, editor],
    tasks=[research_task, writing_task, editing_task],
    process=Process.sequential,  # Execute tasks one after another
    verbose=True,  # Set to True to see detailed agent reasoning in logs
    memory=True   # Enable memory so agents can reference past interactions
)

# Execute the crew with dynamic inputs
result = content_crew.kickoff(inputs={
    "topic": "The Impact of Generative AI on Software Development Productivity in 2024",
    "audience": "Software developers and engineering managers",
    "tone": "Informative yet conversational, with data-driven insights",
    "word_count": "1200"
})

# Access the final output
print(result)
# The result contains the editor's final output — the publication-ready article

The kickoff method accepts a dictionary of inputs that replace the {variable} placeholders in your task descriptions. This makes your crew reusable across different topics, audiences, and tones without modifying agent or task definitions.

Understanding the Execution Flow

When you call kickoff, CrewAI orchestrates a sophisticated workflow:

  1. Task 1 — Research: The research agent receives the topic and uses its tools (SerperDev, WebsiteSearch) to gather information. It reasons about what to search for, executes multiple queries, synthesizes findings, and outputs a structured research brief.
  2. Context Passing: The output of the research task is automatically passed as context to the writing task. The writer agent sees both the original topic parameters and the complete research brief.
  3. Task 2 — Writing: The writer agent crafts the article using the research context, adhering to the specified audience, tone, and word count requirements. It outputs a complete draft.
  4. Context Passing Again: The writer's output flows to the editor as context, along with the research brief (if memory is enabled, the editor can access all prior outputs).
  5. Task 3 — Editing: The editor reviews the draft against the research, applies style rules, fixes issues, and produces the final polished article with an editor's note.

This pipeline ensures each stage builds on the previous one, with full traceability from raw research to final publication.

Adding Advanced Features

Enabling Memory for Cross-Task Awareness

CrewAI's memory feature gives agents access to previous interactions within the crew. When enabled, agents can reference earlier research findings, drafts, or decisions. This is particularly useful when you want the editor to cross-reference the original research brief even though it primarily receives the writer's draft.

content_crew = Crew(
    agents=[researcher, writer, editor],
    tasks=[research_task, writing_task, editing_task],
    process=Process.sequential,
    memory=True,
    memory_config={
        "provider": "mem0",  # Uses mem0 for persistent memory storage
        "user_id": "content-team-session-001"
    }
)

Adding a Human Review Step

You can insert a callback that pauses execution and waits for human input before proceeding to the next task. This is invaluable when you want to approve a draft before the editing stage.

def human_approval_callback(task_output):
    """Present the task output to a human for approval."""
    print("\n" + "="*50)
    print("PENDING HUMAN REVIEW")
    print("="*50)
    print(task_output)
    print("="*50)
    approval = input("Approve and proceed? (yes/no): ")
    if approval.lower() != "yes":
        raise Exception("Human reviewer rejected the output. Stopping execution.")
    return task_output

# Attach callback to the writing task
writing_task = Task(
    description=...,
    agent=writer,
    expected_output=...,
    context=[research_task],
    callback=human_approval_callback  # Pauses after writing for human approval
)

Custom Tools for Specialized Research

Beyond built-in tools, you can create custom tools using CrewAI's BaseTool class. Here's an example of a tool that fetches content from a specific API:

from crewai_tools import BaseTool
from typing import Type
from pydantic import BaseModel, Field
import requests

class WikipediaSearchInput(BaseModel):
    query: str = Field(description="Search query for Wikipedia")

class WikipediaSearchTool(BaseTool):
    name: str = "Wikipedia Search Tool"
    description: str = "Fetches article summaries from Wikipedia for factual research"
    args_schema: Type[BaseModel] = WikipediaSearchInput

    def _run(self, query: str) -> str:
        url = f"https://en.wikipedia.org/api/rest_v1/page/summary/{query.replace(' ', '_')}"
        response = requests.get(url)
        if response.status_code == 200:
            data = response.json()
            return f"Title: {data.get('title')}\nSummary: {data.get('extract')}"
        return f"No Wikipedia article found for '{query}'"

# Add this tool to your researcher agent
researcher.tools.append(WikipediaSearchTool())

Hierarchical Process for Complex Workflows

For larger content operations — like producing a multi-part report or a series of related articles — you can use the hierarchical process. In this mode, a manager agent coordinates the crew, delegating tasks and synthesizing results.

content_crew = Crew(
    agents=[researcher, writer, editor],
    tasks=[research_task, writing_task, editing_task],
    process=Process.hierarchical,  # Manager LLM coordinates execution
    manager_llm="gpt-4-turbo",
    verbose=True
)

In hierarchical mode, the manager agent decides the optimal execution order, can spawn sub-tasks, and validates intermediate outputs before passing them forward. This adds overhead but improves coordination for complex projects.

Complete Working Example

Here is a complete, runnable script that brings together everything covered in this tutorial. Save it as content_crew.py and run it after setting up your environment variables.

# content_crew.py — Complete Content Writing Agent with CrewAI

import os
from dotenv import load_dotenv
from crewai import Agent, Task, Crew, Process
from crewai_tools import SerperDevTool, WebsiteSearchTool

# Load environment variables
load_dotenv()

# Verify API keys are set
assert os.getenv("OPENAI_API_KEY"), "OPENAI_API_KEY not set in .env"
assert os.getenv("SERPER_API_KEY"), "SERPER_API_KEY not set in .env"

# === TOOLS ===
search_tool = SerperDevTool()
web_search_tool = WebsiteSearchTool()

# === AGENTS ===

researcher = Agent(
    role="Senior Research Analyst",
    goal="Conduct thorough, up-to-date research on assigned topics and compile structured findings",
    backstory=(
        "You are a veteran research analyst with 15 years of experience at a leading think tank. "
        "You excel at finding credible sources, extracting key statistics, identifying expert quotes, "
        "and organizing information into clear, logical briefs. You verify facts across multiple sources "
        "and always note the source of each key claim. You present findings in a structured format "
        "with sections for key statistics, expert perspectives, historical context, and current trends."
    ),
    tools=[search_tool, web_search_tool],
    verbose=True,
    allow_delegation=False,
    llm="gpt-4-turbo"
)

writer = Agent(
    role="Senior Content Writer",
    goal="Craft engaging, well-structured articles from research briefs",
    backstory=(
        "You are an award-winning content writer with a decade of experience writing for major publications "
        "like The Atlantic, Wired, and The New Yorker. Your writing style is clear, engaging, and accessible "
        "without sacrificing depth. You know how to hook readers with compelling openings, structure articles "
        "for maximum readability, and end with memorable conclusions. You adapt tone to the target audience "
        "and always cite sources provided in the research brief."
    ),
    verbose=True,
    allow_delegation=False,
    llm="gpt-4-turbo"
)

editor = Agent(
    role="Chief Editor",
    goal="Ensure content meets highest standards of accuracy, clarity, grammar, and brand voice",
    backstory=(
        "You are the chief editor at a prestigious digital publication. You have a hawk-like eye for "
        "grammatical errors, logical gaps, and tonal inconsistencies. You enforce the publication's style guide "
        "religiously — active voice, no jargon without explanation, short paragraphs, and conversational yet "
        "authoritative tone. You provide specific, actionable revision notes and produce a final polished version."
    ),
    verbose=True,
    allow_delegation=False,
    llm="gpt-4-turbo"
)

# === TASKS ===

research_task = Task(
    description=(
        "Research the following topic comprehensively: {topic}\n\n"
        "Your research brief must include:\n"
        "1. A summary of the topic's importance and relevance (2-3 sentences)\n"
        "2. Five to seven key statistics with source attributions\n"
        "3. Three to four expert perspectives or notable quotes on the topic\n"
        "4. Current trends and recent developments (last 12 months)\n"
        "5. Historical context if relevant\n"
        "6. A list of 3-5 credible sources with URLs\n\n"
        "Use the search tools extensively. Verify claims across multiple sources. "
        "Output a structured research brief with clear section headers."
    ),
    agent=researcher,
    expected_output="A comprehensive research brief with all requested sections",
    async_execution=False
)

writing_task = Task(
    description=(
        "Using the research brief provided as context, write a complete article on: {topic}\n\n"
        "Article requirements:\n"
        "- Target audience: {audience}\n"
        "- Tone: {tone}\n"
        "- Word count: approximately {word_count} words\n"
        "- Structure: compelling hook, well-organized body with subheadings, memorable conclusion\n"
        "- Incorporate statistics and expert quotes from the research brief naturally\n"
        "- Cite sources inline where appropriate\n"
        "- Use short paragraphs (3-5 sentences) for readability\n"
        "- Write in active voice, avoid jargon without explanation\n\n"
        "Output the article in clean markdown format."
    ),
    agent=writer,
    expected_output="A complete, polished article draft in markdown format",
    context=[research_task],
    async_execution=False
)

editing_task = Task(
    description=(
        "Edit the article draft provided as context thoroughly.\n\n"
        "1. Verify facts against the research brief\n"
        "2. Correct grammar, punctuation, spelling\n"
        "3. Improve sentence structure and flow\n"
        "4. Ensure consistent tone for audience: {audience}, tone: {tone}\n"
        "5. Check paragraph length (3-5 sentences)\n"
        "6. Verify hook and conclusion quality\n"
        "7. Apply style guide: active voice, no unexplained jargon\n\n"
        "Output final article in markdown with an 'Editor's Note' section summarizing changes."
    ),
    agent=editor,
    expected_output="Final publication-ready article with editor's note",
    context=[writing_task],
    async_execution=False
)

# === CREW ===

content_crew = Crew(
    agents=[researcher, writer, editor],
    tasks=[research_task, writing_task, editing_task],
    process=Process.sequential,
    verbose=True,
    memory=True
)

# === EXECUTION ===

if __name__ == "__main__":
    result = content_crew.kickoff(inputs={
        "topic": "The Impact of Generative AI on Software Development Productivity in 2024",
        "audience": "Software developers and engineering managers",
        "tone": "Informative yet conversational, with data-driven insights",
        "word_count": "1200"
    })
    
    print("\n" + "="*80)
    print("FINAL PUBLICATION-READY ARTICLE")
    print("="*80)
    print(result)
    
    # Optional: save to file
    with open("output_article.md", "w") as f:
        f.write(str(result))
    print("\nArticle saved to output_article.md")

Best Practices for Production Content Crews

Write Detailed Backstories

The backstory is the most important part of agent definition. A vague backstory like "You are a writer" produces mediocre output. Instead, embed specific stylistic preferences, ethical guidelines, and professional standards. For example, tell the writer agent: "You favor concrete examples over abstract explanations. You open articles with a surprising statistic or provocative question. You never use passive voice when active voice is possible." These micro-instructions shape agent behavior far more effectively than adding them to task descriptions.

Keep Tasks Focused and Atomic

Each task should represent one clear phase of the content pipeline. Avoid combining research, writing, and editing into a single task — that defeats the purpose of specialization. If you find a task description becoming too long or covering multiple distinct activities, split it into separate tasks with dedicated agents.

Use Context Wisely

The context parameter in tasks determines information flow. Only pass context that the downstream agent genuinely needs. Passing all prior outputs to every task can overwhelm the agent with noise. In our example, the writer needs the research brief, and the editor needs the writer's draft. This clean chain prevents context pollution.

Implement Output Validation

Use the expected_output field and callback functions to validate intermediate results. If the research task produces a brief missing key statistics, you can catch this in a callback and request regeneration. CrewAI agents are powerful but not infallible — build guardrails.

def validate_research_output(output):
    required_sections = ["statistics", "expert perspectives", "sources"]
    output_lower = output.lower()
    missing = [s for s in required_sections if s not in output_lower]
    if missing:
        raise Exception(f"Research brief missing sections: {missing}. Requesting regeneration.")
    return output

research_task.callback = validate_research_output

Choose the Right LLM Per Agent

Not all agents need the most powerful model. The researcher benefits from a model with strong reasoning and tool-use capabilities (GPT-4 or Claude Opus). The writer needs creative flair (GPT-4 works well). The editor can often use a faster, cheaper model like GPT-3.5-turbo or Claude Haiku for grammar checking — saving costs without sacrificing quality. CrewAI allows per-agent LLM configuration.

editor.llm = "gpt-3.5-turbo"  # Sufficient for grammar and style checking

Cache and Reuse Research

Research is often the most expensive stage due to multiple tool calls. If you're generating multiple articles on related topics, cache research outputs and reuse them. You can implement this at the application level by saving research briefs to a database and feeding them directly to the writer, bypassing the researcher for repeated runs.

Monitor and Log Extensively

Enable verbose=True during development to observe agent reasoning, tool calls, and decision-making. For production, integrate proper logging to track costs, execution times, and success rates. CrewAI exposes events you can hook into for monitoring.

# Example of logging callback
def log_task_execution(task_output, task_name):
    print(f"[LOG] Task '{task_name}' completed at {datetime.now()}")
    print(f"[LOG] Output length: {len(str(task_output))} characters")
    return task_output

Common Pitfalls and How to Avoid Them

Conclusion

Building a content writing agent with CrewAI transforms how you approach content production. Instead of wrestling with monolithic prompts that produce inconsistent results, you assemble a team of specialized AI agents — each with a clear role, professional backstory, and dedicated tools — that collaborate to produce polished, research-backed articles. The framework handles context passing, execution ordering, and memory management, letting you focus on defining the process and quality standards.

Start with the three-agent pipeline described in this tutorial — researcher, writer, editor — and iterate by adding SEO optimization, human review callbacks, or custom research tools as your needs grow. The modular nature of CrewAI means you can swap agents, upgrade models, or add new tasks without restructuring your entire pipeline. Whether you're generating a single high-quality article or scaling to hundreds of SEO-optimized pieces, the principles of specialized agents, atomic tasks, and clean context chains will serve you well. The complete code example provided is ready to run — adapt it, extend it, and build the content engine that fits your workflow.

— Ad —

Google AdSense will appear here after approval

← Back to all articles