◄ ALL POSTS
CrewAI

Multi-Agent AI Systems with CrewAI: Teams Instead of Solo Players

NW Nils Weiser •November 15, 2024 •Updated: 6 Oct 2026 •—

How to orchestrate specialized AI agents with CrewAI – and what changes once the crew runs in production. With lessons from two of my own systems.

A single AI agent quickly hits its limits on complex, multi-step tasks. The obvious answer: multiple specialized agents that work together like a team.

CrewAI is a Python framework built for exactly that, at version 1.15 as of October 2026. I use it in two of my own systems: at Erst Recht, an AI platform for initial legal assessments under German law, and in a research project where an autonomous multi-agent system evaluates studies on dietary supplements. This article covers the basics first, then what is left of them in production. In short: less agent magic, more code.

The Three Core Concepts

Instead of one agent that can do everything, you assemble a team: roles, tasks, process. The examples show a generic crew that triages incoming support requests.

1. Agents: The Specialists

Each agent has a role, a goal, and a backstory. You plug in the model via the LLM class, with a provider prefix in the model name.

from crewai import Agent, LLM

llm = LLM(
    model="provider/model-name",
    max_tokens=4096,
    timeout=120,  # hard ceiling per call
)

analyst = Agent(
    role="Support Analyst",
    goal="Classify requests and assess their urgency",
    backstory="""You analyze support requests for a B2B software product.
    Reply with the assessment only, no introduction.""",
    llm=llm,
    allow_delegation=False,
)

Best Practice: Specific Roles

Instead of "Analyst," use "support analyst for B2B software" with a clearly scoped goal. Just as important is what the agent should not do.

2. Tasks: The Assignments

Tasks define what needs to be done. Via context, a task receives the result of an earlier task; via output_pydantic, you enforce a fixed output format instead of free text.

from crewai import Task

task_assess = Task(
    description="Assess this request: {request}",
    expected_output="Category, priority, and a short rationale",
    agent=analyst,
    output_pydantic=TicketAssessment,  # schema instead of free text
    context=[task_classify],          # result of the previous agent
)

3. Crew: The Team

The crew brings agents and tasks together and defines how they collaborate.

from crewai import Crew, Process

crew = Crew(
    agents=[classifier, analyst, writer],
    tasks=[task_classify, task_assess, task_reply],
    process=Process.sequential,
)

result = crew.kickoff(inputs={"request": text})
assessment = result.tasks_output[1].pydantic  # typed, no regex

The alternative is Process.hierarchical: a manager LLM assigns tasks dynamically and reviews the results. According to the docs, that requires a manager_llm or a manager_agent. In production, I rely on fixed ordering. A process I know in advance is one I can test and debug.

Crews vs. Flows: The Part Tutorials Skip

A crew is a group of agents. A flow is an event-driven process around it: methods with @start() and @listen(), branching with @router(), plus typed state as a Pydantic model. A flow step can run a crew – or plain Python code. CrewAI itself recommends starting with a flow for production. Here is the principle, heavily simplified, in my research project:

from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel

class RunState(BaseModel):
    topic_id: str = ""
    paper_ids: list[str] = []
    evaluation_ids: list[str] = []
    draft_id: str | None = None

class ResearchFlow(Flow[RunState]):
    @start()
    async def discover(self):
        # plain code: load and deduplicate studies
        self.state.paper_ids = await discover_papers(self.state.topic_id)

    @listen(discover)
    async def evaluate(self):
        # fixed scoring formula
        self.state.evaluation_ids = await evaluate_papers(self.state.paper_ids)

    @listen(evaluate)
    async def synthesize(self):
        # LLM step with a fixed output schema
        self.state.draft_id = await write_draft(self.state.evaluation_ids)

    # then: edit → fact-check → validate → publish

What's notable is what's missing: this system contains not a single crew. I had planned one crew per stage. What I built is a set of stages as plain async functions that call the model directly, each with a fixed output schema. From CrewAI, I only use flows there: ordering, state, scheduled start. Every stage has a clear input and a clear output. That leaves little for a self-planning agent to decide.

Two Systems at a Glance

Erst Recht

Several specialized agents, orchestrated with CrewAI, plus a legal knowledge base. The result is a structured initial legal assessment for non-lawyers.

Dietary supplement research project

One flow with fixed stages: find studies, score them, draft the article, edit, fact-check, validate the evidence, publish. The last stage is plain code with no LLM.

Five Rules for Crews in Production

1. A Schema That's Too Strict Can Kill the Whole Run

Hard value constraints in the output schema, such as a Literal, are risky. If the model returns a synonym, Pydantic raises a ValidationError, and depending on the version CrewAI re-raises it out of crew.kickoff(). The whole run aborts, paid tokens are lost, and fallback code after kickoff() never runs.

The fix: the schema describes structure only. The code checks the allowed values after parsing. Here it is for a priority field:

# before: too strict, one synonym kills the whole run
class TicketAssessment(BaseModel):
    priority: Literal["high", "medium", "low"]

# after: schema for structure ...
class TicketAssessment(BaseModel):
    priority: str = Field(description="Allowed: 'high', 'medium', 'low'")

# ... and code for values, after parsing
ALLOWED = {"high", "medium", "low"}

assessment = result.tasks_output[1].pydantic
if assessment.priority.strip().lower() not in ALLOWED:
    flag_for_review(assessment)  # flag the value, the run is preserved

Newer CrewAI versions also offer task guardrails: a function validates the output, and on failure the error goes back to the agent, by default up to three times (guardrail_max_retries). For format errors, that's cleaner. But every retry is another paid model call.

2. Agents Invent Sources

In the research project, every claim in an article has to be backed by paper IDs. The prompt forbids invented IDs. The code still doesn't rely on that:

known_ids = set(paper_abstracts.keys())
for p in parsed.paragraphs:
    support = [pid for pid in p.supporting_paper_ids if pid in known_ids]
    if p.is_claim and not support:
        continue  # a claim without a real source is dropped
    paragraphs.append(...)

Every cited ID is checked against the papers that were actually in the prompt. More hard checks follow: if a claim can't be traced to a real source, the stage fails without retry. The fact-check verifies that cited DOIs exist and that the paper actually supports the claim.

3. No Model Grades Its Own Work

In the research project, the model that writes and the model that checks come from different model families. A model proofreading its own text easily misses the same mistakes it made. The check is mandatory: if it didn't run, nothing gets published, instead of silently downgrading to "unverified."

4. User Input Is Data, Not Instructions

Everything that comes from outside is data, not a command: user input, uploaded documents, abstracts from databases. Such text belongs clearly separated from the instructions in the prompt. In the research project, a deterministic scanner also checks every abstract for typical injection patterns before it reaches a model. Suspicious text doesn't get through unfiltered. Why that's not a luxury is covered in my article on the lethal trifecta.

5. Cost and Runtime Need Hard Limits

In the research project, every model call first passes through a cost governor with fixed limits per run, per day, and per month. If a run breaks its limit, it is aborted. If the month breaks its limit, publishing switches itself off automatically.

Just as important is a timeout on every model call. A hung call must not block a worker. Long crew runs also don't belong in a synchronous API request; the CrewAI docs recommend kickoff_async for that. If you build agents with tools, set the per-agent limits deliberately: max_iter (default: 20), max_execution_time, max_rpm.

Code or Agent? Where I Draw the Line

The most important lesson from both systems: a large part of the work doesn't belong in an agent at all.

Rule of Thumb

Anything with one clearly correct answer belongs in code. Agents get the steps that require judgment. And even there, code checks the result.

And then there's the human. In the original design, the research project was "fully autonomous." What I built is an approval gate: the first articles only go live with human sign-off. How the system turns its own failures into issues and pull requests is described in Loops, Not Prompts.

When You Don't Need a Crew

Checklist for Your First Crew in Production

  1. Start with a flow. Long-lived state belongs in the database, not in flow state.
  2. Every task gets output_pydantic: schema for structure, code for values.
  3. Check every source, ID, and number against the input in code.
  4. Verify with a different model family than the one that wrote.
  5. Add timeouts and cost ceilings before the first user arrives.
  6. Clearly separate user input and external text and treat it as data.
  7. At the start, put a human before publishing.

Conclusion

CrewAI makes getting started easy: roles, tasks, crew, done. In production, the center of gravity shifts. For me, most of the work sits in the code around the agents: schemas that don't abort a run, checks that weed out invented sources, limits that cap costs. The agents take on what requires judgment. Code handles everything else.


Tools used:

CrewAI CrewAI Docs DeepLearning.AI

Planning a multi-agent system, or want to make an existing crew production-ready? Get in touch, I'm happy to help with architecture and implementation.

NW
Nils WeiserAI Agent Specialist · Bodenseeraum
WORK WITH ME ▸

More field notes

ALL POSTS ▸