The problem: a task that divides by trade
A technical article comes from three different jobs: gathering the data, writing, proofreading. If you ask for all three in a single prompt, the model mixes them; if you write the graph of steps yourself as in the article on
LangChain and LangGraph, you write structural code for a flow that is already a chain of roles. CrewAI proposes
another route: you declare agents with a role, a goal and a backstory, you declare tasks with a description and an expected result, you put them in a crew and call kickoff.
The official documentation presents it as an open source framework that unites Crews (teams of autonomous agents) with Flows (event-driven flows that manage state and execution). This
article concentrates on Crews, explains what the parameters really do and where CrewAI takes visibility away from you.
A practical premise before the code: the crewai and crewai-tools packages declare on PyPI Python <3.14,>=3.10 (read on 2026-11-12, version 1.15.41 of both). The examples below
were run with Python 3.10.20 in a temporary virtual environment, because installation is not supported on 3.14. No model was called: we use a subclass of BaseLLM that answers
with fixed text and records the prompts it receives. This way you see how CrewAI builds the prompts, not how a model reasons.
The fake model
CrewAI also accepts as llm an object that extends crewai.BaseLLM. Ours implements call, which receives the messages, saves them in _received_prompts and answers in the
"Thought / Final Answer" format that the agent's loop knows how to interpret; supports_function_calling returns False because this model does not propose tool calls. You recognize here the pattern of the whole series:
a loop that queries the model and interprets the answer, with the model replaced so that the rest can be tested. It serves one purpose only: showing the prompts. It proves nothing about the quality of a real model.
from crewai import BaseLLM
class FakeLLM(BaseLLM):
"""Fake model for CrewAI: it calls no network.
It answers with 'Final Answer' and records every prompt it receives."""
def __init__(self, name="fake", **kw):
super().__init__(model=name, **kw)
self._received_prompts = []
def call(self, messages, tools=None, callbacks=None, available_functions=None,
from_task=None, from_agent=None, response_model=None):
text = messages if isinstance(messages, str) else "\n".join(m["content"] for m in messages)
self._received_prompts.append(text)
role = from_agent.role if from_agent else "?"
return f"Thought: I have everything.\nFinal Answer: [{role}] work completed"
def supports_function_calling(self) -> bool:
return False
The file is called fake_llm.py and the following blocks import it. If you remove the line that records the prompt, everything still works, but you lose the only way you have to see what each agent receives: when debugging a real crew,
it is the first thing you will want.
Agent, Task, Crew: how they read together
An Agent has three mandatory fields: role, goal, backstory. A Task has two: description and expected_output; the agent to assign it to is
optional. A Crew receives agents and tasks and, by default, runs them in sequence. The parameter that links the tasks is context: by listing a previous task, its output is
inserted into the prompt of the task that follows. Placeholders such as {topic} in the descriptions are filled with the dictionary passed to kickoff(inputs=...).
The block builds two agents and two tasks, runs the crew and then prints the prompt actually received by the second agent. It is the most direct proof of what role, goal and
backstory do: they are not metadata, they are prompt text.
import os
os.environ["CREWAI_DISABLE_TELEMETRY"] = "true" # no network traffic to the telemetry service
from crewai import Agent, Task, Crew, Process
from fake_llm import FakeLLM
llm = FakeLLM("fake-1")
analyst = Agent(role="Analyst", goal="Collect data on orders",
backstory="You have worked on the order system for years and you never invent numbers.",
llm=llm, allow_delegation=False)
editor = Agent(role="Editor", goal="Write a clear note",
backstory="You turn data into simple sentences.",
llm=llm, allow_delegation=False)
collection = Task(description="Collect the data on {topic}", expected_output="List of data",
agent=analyst)
note = Task(description="Write a note", expected_output="A three-line note",
agent=editor, context=[collection]) # receives the output of `collection`
crew = Crew(agents=[analyst, editor], tasks=[collection, note], process=Process.sequential)
outcome = crew.kickoff(inputs={"topic": "late orders"})
print("final output :", outcome.raw)
print("model calls:", len(llm._received_prompts))
print("--- prompt received by the Editor ---")
print(llm._received_prompts[-1])
# Output:
# final output : [Editor] work completed
# model calls: 2
# --- prompt received by the Editor ---
# You are Editor. You turn data into simple sentences.
# Your personal goal is: Write a clear note
#
# Current Task: Write a note
#
# This is the expected criteria for your final answer: A three-line note
# you MUST return the actual complete content as the final answer, not a summary.
#
# This is the context you're working with:
# [Analyst] work completed
#
# Provide your complete response:
Run twice (Python 3.10.20, crewai 1.15.41) with identical output. The Editor's prompt begins with "You are Editor.", followed by the backstory, then the goal (goal), the current task, the
expected_output criterion and, under "This is the context you're working with", the Analyst's output: context=[collection] works like this, copying the whole output into the prompt. Two observations: the frame of the prompt is in English
whatever the language of the crew (in the Italian edition of this article the crew is in Italian, so the prompts come out mixed); and since the context is inserted whole, an upstream task with a long output lengthens every downstream prompt. How much it lengthens it depends on your output: here it was not measured in tokens.
What it is worth if you don't write the parameter
Tutorials show parameters such as max_iter or memory without saying the default value, which is what decides the behavior of your crew if you don't write them. The block reads the values from the fields of the Pydantic
models of the installed version, so they do not depend on anyone's memory.
from crewai import Agent, Crew, Task
# What the parameter is worth if you don't write it (read from the Pydantic model fields of crewai 1.15.41)
entries = {Agent: ["verbose", "allow_delegation", "max_iter", "max_rpm", "memory", "cache", "reasoning"],
Crew: ["process", "memory", "cache", "max_rpm", "planning"],
Task: ["context", "async_execution", "human_input", "guardrail"]}
for cls, names in entries.items():
print(cls.__name__)
for name in names:
print(f" {name:18} = {cls.model_fields[name].default!r}")
# Output:
# Agent
# verbose = False
# allow_delegation = False
# max_iter = 25
# max_rpm = None
# memory = None
# cache = True
# reasoning = False
# Crew
# process = <Process.sequential: 'sequential'>
# memory = False
# cache = False
# max_rpm = None
# planning = False
# Task
# context = NOT_SPECIFIED
# async_execution = False
# human_input = False
# guardrail = None
Three values deserve attention. max_iter is 25 per agent: it is the cap on the reasoning and tool turns of a task, and the default is high, so an agent spinning idle consumes many calls before stopping.
cache is True on the agent (tool results are reused) but False on the crew: they are not the same switch. allow_delegation is False: delegation between agents has to be
turned on by hand, and it is best turned on only where needed. As for the parameters on call rate, max_rpm (requests per minute) is None, that is, no limit.
Sequential or hierarchical
There are two processes available: Process.sequential (the tasks in the order you declare them) and Process.hierarchical (a manager agent distributes the work). A list of the values of the enumeration
in the installed version contains only these two; a third "consensual" process, mentioned in older tutorials, does not exist. The documentation adds that for the hierarchical process you must specify manager_llm or a custom
manager_agent. The block checks both things: building the hierarchical crew without a manager fails with a validation error, and with manager_llm CrewAI creates a "Crew Manager" agent by itself with two delegation tools.
import os
os.environ["CREWAI_DISABLE_TELEMETRY"]="true"; os.environ["OTEL_SDK_DISABLED"]="true"
from crewai import Agent, Task, Crew, Process
from fake_llm import FakeLLM
llm = FakeLLM("fake-2")
r = Agent(role="Analyst", goal="Collect data", backstory="Expert.", llm=llm)
t = Task(description="Collect data", expected_output="List", agent=r)
try:
Crew(agents=[r], tasks=[t], process=Process.hierarchical)
except Exception as e:
print(type(e).__name__, "-", str(e).strip().splitlines()[-1].split(" [type=")[0].strip())
c = Crew(agents=[r], tasks=[t], process=Process.hierarchical, manager_llm=llm)
out = c.kickoff()
print(out.raw)
print("manager_agent:", c.manager_agent.role if c.manager_agent else None)
print("manager tools:", [x.name for x in c.manager_agent.tools] if c.manager_agent else None)
# Output:
# ValidationError - Attribute `manager_llm` or `manager_agent` is required when using hierarchical process.
# [Crew Manager] work completed
# manager_agent: Crew Manager
# manager tools: ['Delegate work to coworker', 'Ask question to coworker']
Here the manager is the fake model, which immediately answers "work completed" without delegating: the output shows the structure (the manager created and its Delegate work to coworker and Ask question to coworker tools), not
the behavior of a real manager. You draw a rule of choice from it: in the sequential process you know in advance who does what and in what order; in the hierarchical one you add an agent that decides, hence at least one more call at every coordination step and
a path that is harder to reproduce. If your process can be written as an ordered list, the sequential one is the simplest choice to test.
Tools: decorator and class
CrewAI offers two ways to write your own tools, both in crewai.tools. The @tool("name") decorator transforms a function: the docstring becomes the description the model reads and the argument schema comes
from the type hints. The BaseTool class is for when the tool has state or configuration: you declare name, description, a Pydantic args_schema and the _run method. As in
the LangChain article, the block uses a dictionary in place of a service, to stay runnable offline, and calls run on the tool directly, without an agent.
import os
os.environ["CREWAI_DISABLE_TELEMETRY"]="true"; os.environ["OTEL_SDK_DISABLED"]="true"
from crewai.tools import tool, BaseTool
from pydantic import BaseModel, Field
ORDERS = {"A-1042": "out for delivery"}
@tool("Order status")
def order_status(id: str) -> str:
"""Returns the status of an order given the code (format A-NNNN)."""
return ORDERS.get(id, "order does not exist")
class Input(BaseModel):
id: str = Field(description="Order code, format A-NNNN")
class OrderStatusTool(BaseTool):
name: str = "Order status (class)"
description: str = "Returns the status of an order given the code."
args_schema: type[BaseModel] = Input
def _run(self, id: str) -> str:
return ORDERS.get(id, "order does not exist")
print(type(order_status).__name__, "|", order_status.name)
print(order_status.args_schema.model_json_schema()["properties"])
print(order_status.run(id="A-1042"))
print(OrderStatusTool().run(id="A-9999"))
# Output:
# Tool | Order status
# {'id': {'title': 'Id', 'type': 'string'}}
# out for delivery
# order does not exist
The decorator produces an object of type Tool with the schema {'id': {'title': 'Id', 'type': 'string'}}; the class does the same thing with more code, but gives you a place for state (a connection, a key). For
data, what holds for every agent must be repeated: protection lies in the tool's permissions, not in the sentence asking it to behave. The set of ready-made tools is in the crewai-tools package (listed
in the documentation), to be installed separately; they were not run here because many require a key or the network.
Memory: the setup has changed
Many tutorials describe three separate memories (short term, long term, entity). The documentation consulted on 2026-11-12 describes something else: a unified memory system, a single Memory class that replaces
the separate types, in which a model analyzes the content when it is saved to infer scope, categories and importance, and retrieval combines semantic similarity, recency and importance. It can be used on its own, with a crew (memory=True
or a configured instance) or with an agent. The practical consequence is twofold: memory requires a model (and usually an embedding model), so it has a cost for every save and every recall, and personal data that ends up
in there persists. We did not run it: it needs a real model. The general discussion of memory is in
Memory Systems: Context, History and Knowledge Graphs.
Choosing CrewAI, and when not to
The choice depends on the shape of the problem, not on popularity. The criteria that emerge from what we have seen:
- Yes, when the steps are roles in sequence (research, drafting, review) and you want to declare them rather than program them.
- Yes, for a prototype that has to run soon: it is a few lines, as in the sequential block.
- With caution, when you need to see every step: part of the coordination is inside the framework and the prompts, as you have seen, are composed by it.
- No, when the flow has branches and resumptions over which you want explicit control: there a graph is better, as in LangGraph.
The same documentation proposes combining the two levels: Flows as the structure that governs state, Crews as the unit of work. It is a design indication from the framework, not a universal truth: it is worth
trying only if you already have an event flow to govern. A detail that contradicts older tutorials: CrewAI has asynchronous methods (akickoff native, kickoff_async thread-based), so it is not true that crews are only synchronous.
On the comparison with the other great conversational approach, AutoGen, keep in mind that its documentation now declares the project archived (see
AutoGen and AG2).
Costs, errors, testing
Three habits avoid surprises. Set max_iter explicitly, because the default is 25 and an agent that gets stuck consumes calls at every turn. Write expected_output with precise structure and length: it is
the text that ends up in the prompt as a criterion, and a concise output reduces the context that the following tasks will have to carry along. Test the crew with a fake model as here: you can verify the chain of context,
the process and the prompt without spending anything, and reserve real models for a single, measured trial. The real cost of a crew depends on the model and the texts, and was not measured here. The topic of testing is
taken up in Testing and Evaluation.
Frequently asked questions
Does CrewAI work with Python 3.14?
According to the metadata on PyPI (read on 2026-11-12) crewai 1.15.41 requires Python greater than or equal to 3.10 and less than 3.14. That is why the examples were run with Python 3.10.
What does an agent's backstory really do?
It goes into the prompt: in the prompt recorded by the fake model it appears right after "You are" and the role, before the goal. It is not metadata; writing it well changes the model's input, but the effect on quality depends on the model and was not measured.
How do I pass the output of one task to the next?
With the context parameter of the next task, which lists the tasks to take the output from. CrewAI inserts those outputs into the prompt, whole, under the context heading.
What you can do now
You can describe a team with Agent, Task and Crew, link the tasks with context, and read the prompts that CrewAI builds. You know which default values govern the crew if you don't write them,
when the hierarchical process requires a manager and how to write a tool with a decorator or a class. You can also test everything with a fake model before spending a single call. In the next article,
AutoGen and AG2: Emergent Multi-Agent Conversation, the coordination is not a sequence of roles but a conversation. To compare the two routes with the other architectures, see Multi-Agent Orchestration: Production Architectures.
Sources
- CrewAI - Introduction - Crews and Flows; consulted on 2026-11-12
- CrewAI - Processes - sequential and hierarchical processes, requirement of
manager_llmormanager_agent; consulted on 2026-11-12 - CrewAI - Crews -
akickoffandkickoff_asyncmethods,token_usage; consulted on 2026-11-12 - CrewAI - Memory - unified memory system; consulted on 2026-11-12
- crewai on PyPI - version 1.15.41 and Python requirement; read with the registry JSON API on 2026-11-12. The default values and the behavior of the blocks are those observed with the listings, not read from the documentation

Commenti
Caricamento commenti...
Sign in to comment