--- name: design-agent description: "CrewAI agent design and configuration. Use when creating, configuring, or debugging crewAI agents — choosing role/goal/backstory, selecting LLMs, assigning tools, tuning max_iter/max_rpm/max_execution_time, enabling planning/delegation, setting up knowledge sources, using guardrails, or configuring agents in YAML vs code." --- # CrewAI Agent Design Guide How to design effective agents with the right role, goal, backstory, tools, and configuration. Verified against crewai 1.15.23 on 2026-10-01. Live-tested with real LLMs on 2026-10-01. Exact API forms (imports, defaults, provider extras, structured output) are in the **check-crewai-api** skill, and tools and MCP are in **connect-tools-and-mcp**. Where this skill and those disagree, follow them. --- ## The 80/20 Rule **Spend 80% of your effort on task design, 20% on agent design.** A well-designed task elevates even a simple agent. But even the best agent cannot rescue a vague, poorly scoped task. Get the task right first (see the `design-task` skill), then refine the agent. --- ## 0. How Many Agents Do You Actually Need? **Default to ONE agent.** Add more only when the task genuinely splits into work that requires: - **Different tools or permissions** — e.g. one agent has Slack write access, another reads docs only. - **Different personas the LLM must clearly switch between** — a writer's voice is not a researcher's voice. - **Different LLMs** — a cheap model for mechanical steps, a stronger one for synthesis. - **Different guardrails or output schemas** — separate agents make the contract per stage explicit. **DO NOT add an agent just because the workflow has multiple steps.** A single agent can call multiple tools in sequence within one kickoff (search → scrape → summarize is one agent's loop), produce structured multi-section output in one response, and iterate via its own tool-use loop. **Cost calculus:** every extra agent = at least one more LLM kickoff plus a context handoff. Splitting linear, single-persona work into multiple agents multiplies token cost and adds fragility for marginal quality wins. ### Anti-patterns ❌ Three agents for what is one researcher's job: ```python source_finder = Agent(role="Finds URLs via Firecrawl search", tools=[firecrawl_search]) scraper = Agent(role="Scrapes URLs via Firecrawl scrape", tools=[firecrawl_scrape]) writer = Agent(role="Writes the report") # plus goal, backstory, llm ``` ✅ One researcher does the gathering loop; one writer synthesizes — two agents because the personas and LLMs genuinely differ: ```python researcher = Agent(role="Web Researcher", tools=[firecrawl_search, firecrawl_scrape], llm="anthropic/claude-haiku-4-5") writer = Agent(role="Technical Report Writer", llm="anthropic/claude-sonnet-4-6") ``` ❌ A `Summarizer` agent plus a `Slack Sender` agent (`apps=["slack"]`) to summarize a string and DM it. ✅ One `Slack Reporter` agent with `apps=["slack"]` and a task: "Write a 2-3 sentence executive summary at the top, then DM {recipient_email} the summary followed by the full body." > **Heuristic:** if two "agents" share the same persona, the same tool surface, and the same LLM, they are one agent with a longer task description. ### Once you've decided "one agent is enough" Use `Agent.kickoff()` directly inside a Flow method - no `Crew`, no `Task` ceremony. The Flow owns sequencing and state; each step is a single agent kickoff (Flow mechanics: **build-flow** skill; docs: ). ```python @listen(previous_step) def my_step(self): agent = Agent(role="…", goal="…", backstory="…", tools=[...], llm="anthropic/claude-haiku-4-5") result = agent.kickoff( f"Use this prior step's output: {self.state.prior_field}", response_format=MyPydanticModel, # optional ) self.state.my_field = result.pydantic # or result.raw ``` Reach for `Crew.kickoff()` *only* when a step genuinely benefits from multi-agent collaboration (delegation, hierarchical management, parallel specialists feeding one synthesis), or when the agent needs something that only works inside a crew task: **knowledge sources** and **`max_execution_time`** (Section 2). --- ## 1. The Role-Goal-Backstory Framework Every agent needs three things: **who** it is, **what** it wants, and **why** it's qualified. `{placeholders}` in all three are filled from `crew.kickoff(inputs={...})`. ### Role — Who the Agent Is The role defines the agent's area of expertise. **Be specific, not generic.** | Bad | Good | |---|---| | `Researcher` | `Senior Data Researcher specializing in {topic}` | | `Writer` | `Technical Blog Writer for developer audiences` | | `Analyst` | `Financial Risk Analyst with regulatory compliance expertise` | The role directly shapes how the LLM reasons. A "Senior Data Researcher" will produce different output than a "Research Assistant" even with the same task. ### Goal — What the Agent Wants The goal is the agent's individual objective. It should be **outcome-focused with quality standards**. | Bad | Good | |---|---| | `Do research` | `Uncover cutting-edge developments in {topic} and identify the top 5 trends with supporting evidence` | | `Write content` | `Produce publication-ready technical articles that explain complex topics clearly for non-technical readers` | | `Analyze data` | `Deliver actionable risk assessments with confidence levels and recommended mitigations` | ### Backstory — Why the Agent Is Qualified The backstory establishes expertise, experience, values, and working style. It's the agent's "personality prompt." ```yaml backstory: > You're a seasoned researcher with 15 years of experience in AI/ML. You're known for your ability to find obscure but relevant papers and synthesize complex findings into clear, actionable insights. You always cite your sources and flag uncertainty explicitly. ``` **Include:** years/depth of experience, specific domain knowledge, working style and values ("always cites sources", "prefers concise output"), and the quality standards the agent holds itself to. **Leave out:** implementation details (tools, models, config), task-specific instructions (those go in the task description), and personality traits that don't affect output quality. --- ## 2. Agent Configuration Reference `Agent` silently ignores keyword arguments it does not know, so a misspelled or removed parameter does nothing and raises nothing. Check `"name" in Agent.model_fields` when in doubt. ```python Agent( role="...", goal="...", backstory="...", # Required llm="anthropic/claude-haiku-4-5", # See LLM Selection tools=[...], # Tool instances max_iter=25, # Max reasoning iterations per task (default: 25) max_execution_time=300, # Seconds, crew tasks only (default: None - no limit) max_rpm=10, # Max LLM calls per minute for this agent (default: None) max_retry_limit=2, # Retries when task execution errors (default: 2) allow_delegation=False, # Default: False — agent works alone respect_context_window=True, # Summarize and continue on overflow (default: True) inject_date=False, # Put today's date in the prompt (default: False; format via date_format) verbose=False, # Detailed execution logs (default: False) ) ``` ### Execution limits (measured on 1.15.23) | Limit | Behaviour | |---|---| | `max_iter` | Reaching it does **not** raise. The agent is told to give its final answer now, and the run "succeeds" with whatever it has. With `max_iter=1`, a four-step tool procedure recorded one step and returned an incomplete answer. | | `max_rpm` | Calls over the limit block until the next minute window. With `max_rpm=2`, the third LLM call started at 62 s instead of about 2 s. | | `max_execution_time` | Applies only when the agent runs a `Task` inside a `Crew`; `Agent.kickoff()` ignores it (a 5 s limit with a 15 s tool completed normally after 17 s). In a crew it does **not** stop work at the deadline: the running tool and LLM calls finish, then the task raises `TimeoutError: Task '...' execution timed out after 5 seconds` (raised at 18.8 s), and the finished work is discarded. | So `max_execution_time` turns a slow run into an error but does not stop a hang. To bound wall-clock time, set timeouts where the time is spent: `LLM(model=..., timeout=60)` and timeouts inside your tools, or run the kickoff in a subprocess you can kill. **Tuning `max_iter`:** the default 25 is generous - most tasks finish in 3-8 iterations. Lower it to 10-15 to fail faster when tasks are well-defined. If an agent keeps hitting it, the task is too vague (fix the task, not the limit). Hitting it does not raise, so check outputs, not exit codes. **Other switches:** `respect_context_window=True` summarizes the conversation and continues when the provider reports that the context length was exceeded; with `False` the run stops with `SystemExit: Context length exceeded ...`. `inject_date=True` is worth it for time-sensitive tasks (research, news, scheduling): asked for today's date, an agent with it answered correctly, and one without it answered UNKNOWN. ### Tools - An agent with **no tools** will hallucinate data when asked to search, fetch, or read files - always provide tools for tasks that require external data. - Prefer **fewer, focused tools** - too many tools confuses the agent. - Agent-level tools are available for all tasks the agent performs. `Task(tools=[...])` **replaces** them for that task. In a live test, an agent with a weather tool given a task with only a population tool could call only the population tool. - Prebuilt tools need their keys (`SerperDevTool()` needs `SERPER_API_KEY`). Custom tools, the `crewai_tools` names that really exist, caching and MCP servers: **connect-tools-and-mcp**. ### LLM Selection ```python from crewai import LLM Agent(..., llm="anthropic/claude-haiku-4-5") Agent(..., llm=LLM(model="anthropic/claude-haiku-4-5", temperature=0.2, timeout=60)) ``` - With no `llm=`, the agent uses env `MODEL`, then `MODEL_NAME`, then `OPENAI_MODEL_NAME`, else `gpt-4.1-mini`. That is an OpenAI model, so kickoff fails without `OPENAI_API_KEY`. - Only OpenAI ships with core crewai. `anthropic/...` needs `uv add "crewai[anthropic]"`, `gemini/...` needs `crewai[google-genai]`, and non-native providers need `crewai[litellm]` (full table in **check-crewai-api**). - Give mechanical agents a cheaper model. `function_calling_llm` (a separate model for tool calls) is deprecated on both `Agent` and `Crew` - set each agent's `llm` instead. ### Collaboration Set `allow_delegation=True` only when the agent is part of a crew with other specialized agents, the task genuinely benefits from handing off subtasks, or you're using hierarchical process where the manager delegates. **Warning:** delegation without clear task boundaries leads to infinite loops or wasted iterations. ### Planning (Plan-and-Execute Mode) With a `PlanningConfig`, the agent first generates a plan (a list of steps), executes each step in its own multi-turn loop (capped by `max_step_iterations`), checks each result, then continues, replans, or finishes early. ```python from crewai import Agent from crewai.agent.planning_config import PlanningConfig agent = Agent( role="…", goal="…", backstory="…", tools=[...], planning_config=PlanningConfig(reasoning_effort="medium"), # most common ) ``` To disable planning, omit `planning_config`. `planning=True` alone is shorthand for `PlanningConfig(reasoning_effort="low", max_attempts=1)`. `reasoning=True` is deprecated (DeprecationWarning) - use `planning_config`. | `reasoning_effort` | After each step the planner... | Pick when | |---|---|---| | `"low"` | checks the step with a heuristic (**no extra LLM call**) and continues. No replan, no refine. | You want plan visibility (todos, observations) but trust the agent to follow it linearly. Fastest. | | `"medium"` (default) | observes the step with an LLM call; **replans on failure only**. | The agent's tools can fail (network, exec, scrape) and you want graceful recovery. **The right default for sandbox-coding, research, and other tool-heavy loops.** | | `"high"` | observes, then can finish early, replan fully, or refine the plan after every step. | The task changes shape based on intermediate findings, or you need maximum adaptiveness. Most LLM calls per run. | ```python PlanningConfig( reasoning_effort="medium", observe_steps=None, # None = LLM observation for medium/high, heuristic for low max_steps=20, # cap on planned steps max_replans=3, # full re-plans before finalizing max_attempts=None, # refinement attempts during plan generation max_step_iterations=15, # LLM turns per step step_timeout=None, # wall-clock seconds per step system_prompt=None, plan_prompt=None, refine_prompt=None, # custom prompts llm=None, # separate (cheaper) LLM for planning; else agent.llm ) ``` **When to enable:** for autonomous loops where the agent picks its own steps and you want failure recovery (a coding agent that writes → runs → patches; a research agent that searches → scrapes → revises). **Skip** it for single-tool, single-purpose calls ("summarize this string", "post this Slack DM"). **Cost:** In one measured run, planning added 3-7 calls to a two-step task and wrapped the answer in prose. Measure on your own tasks before turning it on. ### Code Execution `allow_code_execution` and `code_execution_mode` are deprecated no-ops (`allow_code_execution=True` emits a DeprecationWarning), and `CodeInterpreterTool` no longer exists in `crewai_tools`. Give the agent a sandbox tool such as `E2BPythonTool` or `DaytonaPythonTool` instead (see **connect-tools-and-mcp**). ### Agent Guardrails ```python from typing import Any def require_uppercase(result) -> tuple[bool, Any]: if result.raw != result.raw.upper(): return (False, "Rewrite the whole answer in UPPERCASE letters only.") return (True, result.raw) agent = Agent(..., guardrail=require_uppercase, guardrail_max_retries=3) # default retries: 3 result = agent.kickoff("...") # the agent guardrail runs here ``` A guardrail can also be a string, which is checked by an extra LLM call. Agent guardrails have two limits on 1.15.23, both verified with a real LLM: - **They run only on `Agent.kickoff()`.** When the same agent executes a `Task` in a `Crew`, the agent guardrail is never called. - **On crewai 1.15.23 the agent-level guardrail retry re-sent the original prompt without the feedback**, and all three attempts failed the uppercase check above. Prefer a task-level guardrail, which passes the feedback. When retries run out, `kickoff` raises `ValueError: Agent's guardrail failed validation after N retries. Last error: ...`. When output must be fixed and not just rejected, put the guardrail on the **Task** (`Task(guardrail=..., guardrail_max_retries=...)`). The error message is fed back there, and the same uppercase check passed on the second attempt. See `design-task` and **check-crewai-api**. ### Knowledge Sources ```python from crewai.knowledge.source.text_file_knowledge_source import TextFileKnowledgeSource Agent( ..., knowledge_sources=[TextFileKnowledgeSource(file_paths=["company_handbook.txt"])], # read from ./knowledge/ embedder={"provider": "onnx"}, # local, no API key; omit it to use OpenAI embeddings ) ``` Knowledge sources give agents domain-specific data via RAG. Use them when agents need to reference large documents, policies, or datasets. Two things to know: - **Agent knowledge is only queried when the agent runs a task inside a `Crew`.** `Agent.kickoff()` silently ignores `knowledge_sources`. In a live test, the same agent answered "UNKNOWN" from `kickoff()` and correctly from a one-task crew. - Without `embedder=`, knowledge uses OpenAI embeddings. With no `OPENAI_API_KEY`, agent knowledge raises `ValueError: Invalid Knowledge Configuration` at crew kickoff. Crew-level knowledge only logs `Failed to upsert documents` and runs **without** it. Embedders that work without OpenAI, `KnowledgeConfig`, memory and storage paths: [references/memory-and-knowledge.md](references/memory-and-knowledge.md). --- ## 3. YAML Configuration (Recommended) Define agents in `agents.yaml` for clean separation of config and code. Create a YAML project with `crewai create crew --classic`. Without `--classic`, `crewai create crew` starts an interactive wizard that writes a JSON project instead. ```yaml researcher: role: > {topic} Senior Data Researcher goal: > Uncover cutting-edge developments in {topic} with supporting evidence and source citations backstory: > You're a seasoned researcher with 15 years of experience. Known for finding obscure but relevant sources and synthesizing complex findings into clear insights. You always cite your sources and flag uncertainty. # Optional overrides: llm: anthropic/claude-haiku-4-5, max_iter: 15, max_rpm: 10, # allow_delegation: false, verbose: true ``` Then wire in `crew.py`: ```python @CrewBase class MyCrew: agents_config = "config/agents.yaml" tasks_config = "config/tasks.yaml" @agent def researcher(self) -> Agent: return Agent(config=self.agents_config["researcher"], tools=[SerperDevTool()]) ``` **Critical:** The method name (`def researcher`) must match the YAML key (`researcher:`). Mismatch causes `KeyError` (verified: `KeyError: 'researcher'`). Verified live: `llm` and `max_iter` set in YAML are applied, `{topic}` in the role is filled at kickoff, and tools attach in Python. Keep tools, guardrail functions and Pydantic models in Python; YAML holds the text and scalar settings. --- ## 4. Agent.kickoff() — Direct Agent Execution Use `Agent.kickoff()` when you need one agent with tools and reasoning, without crew overhead. This is the most common pattern in Flows. It does not use the agent's knowledge sources or `max_execution_time` (Section 2). ```python from pydantic import BaseModel from crewai import Agent researcher = Agent( role="Senior Research Analyst", goal="Find comprehensive, factual information with source citations", backstory="Expert researcher known for thorough, evidence-based analysis.", tools=[...], llm="anthropic/claude-haiku-4-5", ) result = researcher.kickoff("What are the latest developments in quantum computing?") print(result.raw) # str — the agent's full response print(result.usage_metrics) # dict: total_tokens, prompt_tokens, completion_tokens, ... class ResearchFindings(BaseModel): key_trends: list[str] sources: list[str] confidence: float result = researcher.kickoff("Research the latest AI agent frameworks", response_format=ResearchFindings) print(result.pydantic.key_trends, result.pydantic.confidence) result = await researcher.kickoff_async("...", response_format=ResearchFindings) # async variant ``` > **Note:** `Agent.kickoff()` returns `LiteAgentOutput` - access structured output via `result.pydantic`. This differs from `llm.call(messages, response_model=Model)`, which returns the Pydantic object directly. `Agent.kickoff(..., response_model=...)` is a `TypeError`. **File inputs** need an extra: `uv add "crewai[file-processing]"`, then `from crewai_files import FileInput` and `researcher.kickoff("Summarize this document", input_files={"document": FileInput(path="report.pdf")})`. Without the extra, the import fails with `ModuleNotFoundError: No module named 'crewai_files'`. **Agent.kickoff() vs Crew.kickoff():** use `Agent.kickoff()` when each step is a distinct agent and a Flow controls sequencing (the Section 0 shape - verified live as a two-step Flow). Use `Crew.kickoff()` when multiple agents collaborate on related tasks within a single step, or the agent needs knowledge sources or `max_execution_time`. ### Agents in Conversational Flow Routes In conversational Flows (`from crewai.flow import ConversationState`), the Flow owns the chat lifecycle and route selection. Call agents inside route handlers for bounded tool-backed work: research, docs lookup, account actions, triage, drafting, or escalation prep. The mechanics (`handle_turn`, `ConversationConfig`, a tested example) are in the **build-flow** skill's conversational-flows reference. Design implications: - Keep the conversational `Flow` responsible for session id, message history, routing, trace finalization, and approvals. - Keep each agent narrow: one route, one tool surface, one job. - Use `self.append_agent_result(name, result, visibility="private")` for scratch work that should not enter canonical chat history. - Return the user-visible reply from the handler (or call `self.append_assistant_message(reply)`) so the next turn has the assistant context. - Do not make a "chat agent" with every tool. Route first, then invoke a focused agent for the selected route. --- ## 5. Specialist vs Generalist Agents > Apply this section *after* you've decided you genuinely need multiple agents (Section 0). With one agent, the only question is how to design that agent. **When you do need multiple agents, prefer specialists.** An agent that does one thing well outperforms one that does many things acceptably. Use a specialist when the task needs deep domain knowledge, quality matters more than speed, or the task is complex enough to benefit from focused expertise. A generalist is acceptable for simple tasks with clear instructions, for prototyping you'll specialize later, and for tasks that truly span several domains equally. Instead of one "Content Writer" agent, create `technical_writer` (technical accuracy, code examples), `copywriter` (persuasive, audience-focused copy) and `editor` (grammar, consistency, style guide). Each has a narrow role, specific goal, and a backstory that reinforces that expertise. --- ## 6. Agent Interaction Patterns **Sequential (default):** `Researcher → Writer → Editor`. Agents work one after another, and each receives prior outputs as context. Best for linear pipelines where each step builds on the last. **Hierarchical:** a manager agent delegates and validates; task assignment is dynamic. Best for complex workflows where assignment depends on intermediate results. ```python Crew( agents=[researcher, writer, editor], tasks=[research_task, writing_task, editing_task], process=Process.hierarchical, manager_llm="anthropic/claude-haiku-4-5", # or manager_agent=...; one is required ) ``` Without `manager_llm` or `manager_agent`, `Crew(...)` raises a `ValidationError`. **Agent-to-agent delegation:** with `allow_delegation=True`, an agent gets two tools, `Delegate work to coworker` and `Ask question to coworker`, that name the other crew members. Verified live: asked to get a sentence from the Writer, a lead agent called `ask_question_to_coworker` and returned the Writer's answer. --- ## 7. Common Agent Design Mistakes | Mistake | Impact | Fix | |---|---|---| | Generic role like "Assistant" | Agent produces unfocused, shallow output | Use specific expertise: "Senior Financial Analyst" | | No tools for data-gathering tasks | Agent hallucinates data instead of searching | Always add tools when the task requires external info | | Too many tools (10+) | Agent gets confused choosing between tools | Limit to 3-5 relevant tools per agent | | Backstory full of task instructions | Agent mixes personality with task execution | Keep backstory about WHO the agent is; task details go in the task | | `allow_delegation=True` by default | Agents waste iterations delegating trivially | Only enable when delegation genuinely helps | | max_iter too high for simple tasks | Agent loops unnecessarily on vague tasks | Lower max_iter; fix the task description instead | | No guardrail on critical output | Bad output passes through unchecked | Add a Task guardrail for outputs that feed into production systems | | Agent guardrail on an agent used in a Crew | Guardrail never runs | Put the guardrail on the Task | | Trusting `max_execution_time` to stop a hang | It raises only after the slow call returns, and `Agent.kickoff()` ignores it | Add LLM and tool timeouts | | Knowledge sources on an agent run via `Agent.kickoff()` | Knowledge silently unused | Run it as a crew task, or put the facts in the prompt | | No `llm=` and no OpenAI key | Kickoff fails: the default is OpenAI `gpt-4.1-mini` | Set `llm=` (or env `MODEL`) and install the provider extra | | Planning on a simple task | 2-3x the LLM calls, sometimes a worse answer | Enable planning only for open-ended tool loops | --- ## 8. Agent Design Checklist Before deploying an agent, verify: - [ ] **Role** is specific and domain-focused (not "Assistant" or "Helper") - [ ] **Goal** includes desired outcome AND quality standards - [ ] **Backstory** establishes expertise and working style - [ ] **Tools** are assigned for any task requiring external data - [ ] **No excess tools** — 3-5 per agent maximum - [ ] **max_iter** is tuned for expected task complexity (10-15 for simple, 20-25 for complex) - [ ] **Timeouts** are set where time is spent (LLM `timeout`, tool timeouts); `max_execution_time` is only a backstop for crew tasks - [ ] **Guardrails** for critical outputs are on the Task - [ ] **LLM** is set explicitly, fits the task's complexity, and has its provider extra installed - [ ] **Knowledge** has an explicit `embedder`, and the agent runs inside a crew - [ ] **Delegation** is disabled unless genuinely needed --- ## References For deeper dives into specific topics, see: - [Custom Tools](references/custom-tools.md) — building your own tools with `@tool` decorator and `BaseTool` subclass - [Memory & Knowledge](references/memory-and-knowledge.md) - memory, knowledge sources, embedders that work without OpenAI, storage, scoping For related skills: - **check-crewai-api** - current imports, parameters, defaults and provider extras - **connect-tools-and-mcp** - custom tools, real `crewai_tools` names, caching, MCP servers - **build-flow** - Flow state, routing, persistence, conversational flows - **getting-started** - project scaffolding, choosing the right abstraction - **design-task** — task description/expected_output best practices, guardrails, structured output, dependencies - **test-crewai-project** - testing agents and crews offline with a stub LLM - **ask-docs** - query the live CrewAI docs for questions not covered by these skills