Anatomy of a Task
Two fields drive output quality more than anything else: description and expected_output.
Full task definition
from crewai import Task
analyze_task = Task(
description=(
"Analyze the customer churn data in {csv_path}. "
"Identify the top 3 segments with churn > 15% and explain why."
),
expected_output=(
"A markdown table with columns: segment, churn_rate, primary_driver. "
"Followed by a 3-sentence explanation per segment."
),
agent=data_analyst,
context=[], # outputs of prior tasks to inject
output_file="churn_report.md",
async_execution=False,
)
Writing Descriptions
Be specific. Reference the inputs by name with {placeholders}. State constraints up front.
❌ Vague
"Write something about {topic}"
✓ Specific
"Write a 300-word LinkedIn post about {topic} aimed at engineering managers. Include 1 stat, 1 example, and end with a question."
Expected Output: The Quality Lever
expected_output tells the agent what "done" looks like. Be concrete about format, length, and structure — agents that know the shape of the answer produce it more reliably.
Bad vs Good expected_output
# ❌ Bad — agent guesses the format
expected_output="Some research notes"
# ✓ Good — leaves nothing to interpretation
expected_output=(
"A markdown bullet list of exactly 5 facts. "
"Each bullet: '- **Fact**: explanation (source: URL)'. "
"No introduction, no conclusion."
)
Passing Context Between Tasks
The context parameter injects outputs of earlier tasks into the prompt of the current one. This is how a writer "sees" what the researcher found.
Chained tasks
research = Task(description="Research X", expected_output="...", agent=researcher)
outline = Task(description="Outline an article", expected_output="...", agent=outliner, context=[research])
draft = Task(description="Write the article", expected_output="...", agent=writer, context=[research, outline])
edit = Task(description="Polish the draft", expected_output="...", agent=editor, context=[draft])
💡 Tip: In sequential mode, context is automatic for adjacent tasks. Use explicit context=[...] when you need a non-adjacent task's output, or in hierarchical mode.
Async Tasks for Parallelism
Independent tasks (e.g. researching three sub-topics in parallel) can run concurrently with async_execution=True.
Parallel research, sequential synthesis
research_a = Task(description="Research topic A", agent=r1, async_execution=True, expected_output="...")
research_b = Task(description="Research topic B", agent=r2, async_execution=True, expected_output="...")
research_c = Task(description="Research topic C", agent=r3, async_execution=True, expected_output="...")
synthesize = Task(
description="Combine the three research outputs into one report",
agent=writer,
context=[research_a, research_b, research_c],
expected_output="A unified report.",
)
Structured Outputs (Pydantic)
For programmatic consumption, define a Pydantic model and pass it as output_pydantic. CrewAI will coerce the response.
Typed task output
from pydantic import BaseModel
from crewai import Task
class Finding(BaseModel):
fact: str
source: str
class ResearchOutput(BaseModel):
findings: list[Finding]
research_task = Task(
description="Research {topic}",
expected_output="A ResearchOutput object with 5 findings",
agent=researcher,
output_pydantic=ResearchOutput,
)
result = crew.kickoff(inputs={"topic": "RAG"})
parsed: ResearchOutput = result.pydantic
for f in parsed.findings:
print(f.fact, "→", f.source)
Notes
Treat expected_output like an API response schema
Agents optimize toward what you reward in the contract. If expected_output is vague, you get prose that is hard to parse downstream. Specify structure, tone, length bounds, and must-include fields.
Context lists grow quietly
Passing prior task outputs into context is convenient but can balloon tokens. Summarize or extract structured facts before chaining long pipelines, especially when using frontier models.
Async kickoffs need backpressure
Concurrent task execution can overwhelm provider rate limits or your own worker pool. Add semaphores, retries with jitter, and per-tenant concurrency caps when exposing async crews behind APIs.
Human-readable outputs are not machine-readable
If another agent must consume JSON, say so explicitly and reject markdown fences in expected_output. Otherwise you will spend debugging cycles on brittle string cleanup.
CrewAI tasks FAQ
What makes a good CrewAI task description?
Good descriptions name inputs, constraints, and done criteria in plain language. They tell the agent what evidence to gather before declaring the task complete.
What is expected_output in CrewAI?
expected_output is the acceptance contract: format, length, sections, and tone. It is how downstream agents and humans verify a task without re-reading the entire prompt stack.
How does context pass between CrewAI tasks?
Downstream tasks can reference outputs from upstream tasks in their descriptions or structured context fields so each step sees the right artifacts without duplicating work.
Can CrewAI tasks run asynchronously?
Yes, when you use async APIs and async-compatible tools, tasks can overlap for independent work. Keep dependencies explicit so ordering bugs do not slip into production.
How do I test CrewAI tasks?
Create a small golden set of inputs, assert shape against expected_output, and log failures with prompts and tool traces so regressions are easy to replay.
See also: DevShelfHub's CrewAI tool review for a product-level comparison, pricing notes, and links back into this tutorial series.