Good crewai task design context expected output definitions separate a prototype that demos on Slack from a production multi-agent system that survives silent LLM drift. A task with no context contract forces agents to guess what upstream produced; a task with no expected output contract forces you to scrape strings from the logs. Treat tasks as typed functions in a pipeline, and the rest of the crew becomes testable.
1. Treat a task as a typed unit of work
CrewAI’s Task object is the smallest executable unit in a crew. It binds an Agent to a description, an optional context list, and an output spec. The mistake most teams make is writing description as if it were a one-off prompt to ChatGPT. It is not. It is a persistent node in a graph that may run after other nodes, feed its result forward, and get re-executed during debugging.
A minimal task looks like this:
from crewai import Agent, Task
researcher = Agent(
role="Researcher",
goal="Find current LLM inference pricing trends",
backstory="You track GPU economics and API gateways.",
)
research_task = Task(
description="Identify three pricing changes in LLM gateways during 2024.",
expected_output="Three bullet points, each with a vendor name and a one-line summary.",
agent=researcher,
)
The expected_output field is not decorative. CrewAI injects it into the system prompt as a formatting directive. If you omit it, the agent returns free text and your downstream parser becomes a regex nightmare.
2. Wire context explicitly, not through agent memory
In crewai task design context expected output must be explicit; the context parameter is the only deterministic way to pass data between tasks. It accepts a list of Task objects whose outputs have already been produced. CrewAI serializes those outputs and prepends them to the dependent task’s prompt under a clear Context section.
writer = Agent(
role="Writer",
goal="Summarize research into a brief",
backstory="You write concise internal memos.",
)
write_task = Task(
description="Write a 150-word memo from the research findings.",
expected_output="Markdown memo with a heading and three paragraphs.",
context=[research_task],
agent=writer,
)
How context injection works
When write_task runs, CrewAI replaces the context slot with the string output of research_task. The writer agent never sees the researcher’s internal reasoning—only the final artifact. This is exactly what you want: tight coupling on outputs, loose coupling on process.
Pitfall: relying on agent memory
Agents maintain short-term memory within a single task, but cross-task handoffs via memory are non-deterministic and silently drop data under token pressure. I have debugged crews where the writer “forgot” a key statistic because the researcher’s memory buffer was truncated. Use context. If the data is large, add a summarization task in between rather than hoping memory survives.
Pitfall: context token blowup
Every token in an upstream output is re-sent to the downstream agent. If research_task returns 4,000 tokens and three tasks consume it, you pay for 12,000 input tokens. Design upstream tasks to emit tight artifacts. An expected_output that demands “a concise table” pays for itself.
3. Define expected output as a contract
Natural-language contract
For human-in-the-loop steps, a precise expected_output string is enough:
review_task = Task(
description="Review the memo for factual errors.",
expected_output="Either 'APPROVED' or a list of specific corrections with line references.",
context=[write_task],
agent=reviewer,
)
This is flexible and cheap. The tradeoff is that you must parse it yourself.
Structured contracts with Pydantic
When another system consumes the result, demand structure. CrewAI supports output_pydantic to parse the model response into a typed object.
from pydantic import BaseModel
class Correction(BaseModel):
line: int
issue: str
fix: str
class ReviewResult(BaseModel):
status: str # "APPROVED" or "REJECTED"
corrections: list[Correction]
review_task = Task(
description="Review the memo for factual errors.",
expected_output="Structured review with status and optional corrections.",
output_pydantic=ReviewResult,
context=[write_task],
agent=reviewer,
)
Wrap lists in a container model. CrewAI’s parser expects a single root object; handing it list[Correction] directly will fail or silently drop items.
Tradeoff: rigidity vs parsing cost
Structured output reduces your parsing code to zero, but it constrains the model. A weak model may emit invalid JSON and CrewAI will raise a ValidationError. You trade a little latency and a higher error rate for machine-grade reliability. For critical paths, add a retry wrapper or a fallback agent that re-prompts with the schema pasted inline.
4. Compose and execute the crew
Tasks declare edges; the Crew object schedules them. By default, CrewAI runs tasks sequentially in the order listed, respecting context dependencies.
from crewai import Crew
crew = Crew(
tasks=[research_task, write_task, review_task],
verbose=True,
)
result = crew.kickoff()
print(result.raw) # final task output
If you need parallelism, set process="hierarchical" or use async execution, but be aware that parallel branches cannot share context unless you explicitly join them with a merge task. I prefer explicit sequential flows until profiling proves a bottleneck.
5. Make tasks resilient to LLM outages
LLM providers fail. Rate limits hit, regions degrade, and a single 429 can stall your entire crew. If you point CrewAI’s LLM client at n4n.ai, a single OpenAI-compatible endpoint, you get automatic fallback across providers when one is rate-limited, so your task definitions and expected_output contracts stay unchanged. Under the hood, CrewAI uses LiteLLM, so setting OPENAI_API_BASE and OPENAI_API_KEY to the gateway is sufficient.
import os
os.environ["OPENAI_API_BASE"] = "https://api.n4n.ai/v1"
os.environ["OPENAI_API_KEY"] = os.getenv("N4N_KEY")
# CrewAI now routes through the gateway with fallback
Even with fallback, design tasks to be idempotent. A task that appends to a file instead of overwriting will duplicate data on retry. Make description state the side effect explicitly: “Write the memo to memo.md, overwriting any existing content.”
6. Test tasks in isolation
A crew is only as debuggable as its weakest task. Before running the full pipeline, execute each task alone with a fixed input context.
# Stub context by manually setting output
research_task.output = "Vendor A cut prices 20% in Q1."
write_task.context = [research_task]
partial = Crew(tasks=[write_task]).kickoff()
assert "memo" in partial.raw.lower()
This catches prompt drift, schema mismatches, and context formatting bugs without burning tokens on the whole chain.
Common pitfalls in crewai task design context expected output
- Vague expected_output: “Something useful” is not a contract. Specify format, length, and edge cases.
- Circular context: Task A depends on B, B depends on A. CrewAI will hang or error; draw the graph first.
- One agent for all roles: Reusing a generalist agent across tasks removes the role grounding that makes outputs consistent.
- Ignoring output size: A task that returns a 10k-token report fed into five others will blow your context window and your budget.
Good crewai task design context expected output discipline turns a fuzzy multi-agent demo into a pipeline you can ship. Define edges with context, enforce boundaries with expected_output, and test each node like a unit. The models will still surprise you, but the surprises will be localized, not systemic.