n4nAI

CrewAI real-world example: automated blog writing crew

Build a working CrewAI blog writing crew example with researcher, writer, and editor agents using OpenAI-compatible LLMs and verify the output locally.

n4n Team3 min read720 words

Audio narration

Coming soon — every post will get a voice note here.

Shipping a multi-agent content pipeline sounds abstract until you need a draft by Friday. This crewai blog writing crew example wires up a researcher, a writer, and an editor that turn a single topic into a publishable Markdown post with SEO metadata. The pattern below runs locally and scales to any OpenAI-compatible endpoint.

Step 1: Set up the environment and dependencies

Create an isolated environment so you don’t pollute your system Python. CrewAI pulls in LangChain primitives, so install those together.

python -m venv .venv
source .venv/bin/activate
pip install crewai langchain-openai python-dotenv

Store credentials in a .env file. CrewAI reads OPENAI_API_KEY by default if you use the OpenAI wrapper, but we’ll pass the key explicitly to avoid surprises.

OPENAI_API_KEY=sk-your-key-here
# Optional: point at a gateway instead of OpenAI directly
OPENAI_BASE_URL=https://api.openai.com/v1

Verify the install by importing the package:

python -c "import crewai; print(crewai.__version__)"

If that prints a version string (e.g., 0.28.0 or newer), you’re ready.

Step 2: Configure the LLM backend

CrewAI agents accept any LangChain chat model. We’ll use ChatOpenAI because it speaks the OpenAI protocol and lets us set base_url. This is the seam where you can swap providers without touching agent logic.

from langchain_openai import ChatOpenAI
import os

BASE_URL = os.getenv("OPENAI_BASE_URL", "https://api.openai.com/v1")
API_KEY = os.getenv("OPENAI_API_KEY")

llm = ChatOpenAI(
    model="gpt-4o-mini",
    temperature=0.4,
    base_url=BASE_URL,
    api_key=API_KEY,
)

If you want model redundancy and per-token metering without juggling provider keys, point BASE_URL at n4n.ai’s OpenAI-compatible endpoint, which fronts 240+ models and fails over automatically when a provider is rate-limited or degraded. The agent code stays identical.

For a real blog crew, consider using a stronger model for the editor and a cheaper one for research. You can instantiate two LLMs:

research_llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.2, base_url=BASE_URL, api_key=API_KEY)
edit_llm = ChatOpenAI(model="gpt-4o", temperature=0.3, base_url=BASE_URL, api_key=API_KEY)

Step 3: Define the agents

A crewai blog writing crew example needs clear role separation. The researcher gathers facts, the writer drafts, the editor polishes. Each agent gets a goal and backstory; these strings are injected into the prompt, so be specific.

from crewai import Agent

researcher = Agent(
    role="Technical Researcher",
    goal="Find accurate, current facts and code patterns for the given topic",
    backstory="A meticulous engineer who cites sources and avoids hype.",
    llm=research_llm,
    verbose=True,
    allow_delegation=False,
)

writer = Agent(
    role="Technical Writer",
    goal="Turn research notes into a clear, skimmable Markdown draft",
    backstory="A staff engineer who writes like a human, not a marketer.",
    llm=research_llm,
    verbose=True,
    allow_delegation=False,
)

editor = Agent(
    role="Senior Editor",
    goal="Enforce structure, remove fluff, and add SEO title/meta",
    backstory="A pragmatic lead who cares about reader time and search visibility.",
    llm=edit_llm,
    verbose=True,
    allow_delegation=False,
)

verbose=True streams agent thoughts to stdout. Keep it on during development; disable in production to reduce log volume.

Step 4: Define tasks and dependencies

Tasks are the units of work. The context parameter wires task outputs into later prompts. In this crewai blog writing crew example, the writer consumes the researcher’s bullets, and the editor consumes the writer’s draft.

from crewai import Task

topic = "using vector indexes for low-latency RAG"

research_task = Task(
    description=f"Research the topic: {topic}. Collect 3-5 concrete implementation details, with code snippets if public. Note trade-offs.",
    expected_output="Bullet list of verified facts and a short source list.",
    agent=researcher,
)

write_task = Task(
    description="Write an 800-1200 word Markdown post from the research. Use ## headings, short paragraphs, and at least one code block. Include YAML front matter with title and description.",
    expected_output="Markdown string with front matter.",
    agent=writer,
    context=[research_task],
)

edit_task = Task(
    description="Edit the draft. Enforce active voice, kill filler, verify code correctness, and ensure the meta description is 130-160 characters. Output final Markdown only.",
    expected_output="Final Markdown string ready for publish.",
    agent=editor,
    context=[write_task],
)

The expected_output field is not a validator; it guides the model. You still need code-side checks (see Step 6).

Step 5: Assemble and run the crew

CrewAI supports sequential and hierarchical processes. For a linear pipeline, sequential is correct and predictable.

from crewai import Crew, Process

crew = Crew(
    agents=[researcher, writer, editor],
    tasks=[research_task, write_task, edit_task],
    process=Process.sequential,
    verbose=True,
)

result = crew.kickoff()
print(result)

kickoff() blocks until the final task completes. The returned object has a .raw attribute with the editor’s string. In a script, capture it:

with open("post.md", "w") as f:
    f.write(result.raw)

Run with python crew.py. You’ll see each agent’s reasoning in the logs, then the final post printed.

Step 6: Verify the output

Model output is probabilistic. Add a lightweight assertion harness to fail the build if the structure is wrong. This is how you make the crewai blog writing crew example safe for CI.

import re

def verify(md: str):
    assert md.strip().startswith("---"), "Missing YAML front matter"
    assert "title:" in md[:200], "Front matter missing title"
    assert "description:" in md[:200], "Front matter missing description"
    assert md.count("## ") >= 3, "Need at least 3 section headings"
    words = len(re.findall(r"\b\w+\b", md))
    assert 600 <= words <= 1500, f"Word count out of range: {words}"
    desc_match = re.search(r"description:\s*(.+)", md)
    if desc_match:
        assert 130 <= len(desc_match.group(1).strip()) <= 160, "Meta description length off"
    print(f"Verification passed: {words} words")

verify(result.raw)

If assertions pass, you have a draft that meets basic editorial specs. Open post.md and read it; the final check is human.

Production considerations

The toy pipeline above uses one LLM call per task. Real content pipelines need guardrails:

  • Caching: Wrap the researcher with a disk cache keyed by topic hash. CrewAI tasks are pure functions of inputs; cache the research_task output to skip re-search on reruns.
  • Rate limits: Sequential crews rarely hit limits, but parallel ones will. Use exponential backoff or a gateway that queues.
  • Model routing: Assign cheaper models to research and drafting, reserve expensive ones for editing. Pass different llm objects per agent as shown.
  • Token accounting: If you bill internal teams, capture usage. Gateways that provide per-token metering let you attribute cost to each crew run without custom instrumentation.
  • Determinism: Set temperature=0 for researcher and editor if you need reproducible output for compliance.

A crewai blog writing crew example is also a good testbed for tool use. Give the researcher a SerperDevTool or a custom HTTP tool to fetch live docs. The writer then works from grounded context, reducing hallucination.

Troubleshooting

Agents loop or delegate unexpectedly. Set allow_delegation=False unless you explicitly want hierarchical handoff.

Empty output from kickoff(). Check that expected_output is set on every task. CrewAI relies on it to format the final message.

LangChain version conflicts. CrewAI moves fast; pin langchain-openai==0.1.7 and crewai==0.28.0 if you hit import errors, then upgrade deliberately.

Base URL rejected. Confirm the endpoint speaks /v1/chat/completions and that your key has credits. OpenAI-compatible gateways should work without code changes.

The pattern here—research, draft, edit, verify—extends to release notes, API docs, and changelogs. Swap the topic variable and agents’ backstories; the orchestration stays the same.

Tagscrewaireal-world-examplescontent-pipelineuse-case

Written by

n4n Team

The team building n4n — a single OpenAI-compatible API in front of 240+ models, with automatic fallback, load balancing and pay-per-token metering.

More from n4n Team →

All crewai real-world crew examples posts →