Module 3 — Tasks, dependencies and expected outputs
Agents describe who. Tasks describe what. In CrewAI, a Task is a bundle of instructions that names its own agent, states the concrete deliverable it produces, lists which previous tasks feed into it, and — most importantly — declares an expected_output precise enough that a reviewer could tick it or reject it. That last field is where most crews fall apart, and where this module spends its time.
The three fields you cannot skip
A minimal task looks like this:
from crewai import Task
extract_features = Task(
description=(
"Read the product brief in the file 'brief.md'. Produce an exhaustive "
"list of every feature, constraint and audience note that the brief "
"explicitly states. Do not invent features. Do not group them yet."
),
expected_output=(
"A JSON object with three keys: features (array of {name, source_line}), "
"constraints (array of strings), audience (array of strings). "
"Every feature must cite the line number in the brief where it appears."
),
agent=analyst,
)
Three fields carry the weight. description is the instruction the agent receives; write it like a task ticket, verb first, and be brutal about what is out of scope ("Do not invent features"). agent is the crew member who executes it — one task, one agent, no ambiguity. expected_output is the contract that will be checked against the actual result. It is the single most powerful lever in the whole framework: a vague expected output produces a vague result, an over-specified one causes retries; a shape that mentions the exact keys and types produces something a downstream task can consume without parsing gymnastics.
Passing context: the context list
The Writer needs the feature list. Rather than paste it into the description, we declare the dependency:
draft_sections = Task(
description=(
"Draft the ten documentation sections listed in the house outline. "
"Use only the features and constraints from the previous task. "
"One paragraph per feature, no feature invented."
),
expected_output=(
"A JSON array of ten objects: {section_title, body_markdown, features_used}. "
"features_used must reference feature names present in the previous task's output."
),
agent=writer,
context=[extract_features],
output_file="drafts/sections.json",
)
The context list tells CrewAI to prepend the outputs of the named tasks to this task's prompt. Two consequences worth naming. First, the Writer sees the Analyst's JSON as part of its input, not as an opaque tool call — it can quote from it directly. Second, when a task in context fails or returns malformed output, the failure propagates: the Writer will not be asked to invent a feature list to keep going. That is a feature, not a bug.
Do not smuggle context through the description string. It works today, it breaks the moment you reorder tasks, and it makes the trace hard to read.
Structured outputs beat prose contracts
CrewAI 2026 supports Pydantic and JSON schemas as the output type. That is what you want the moment a downstream task consumes the result:
from pydantic import BaseModel
from typing import List
class Feature(BaseModel):
name: str
source_line: int
class Extraction(BaseModel):
features: List[Feature]
constraints: List[str]
audience: List[str]
extract_features = Task(
description="...",
expected_output="Structured extraction of the brief",
agent=analyst,
output_pydantic=Extraction,
)
Two things happen. First, CrewAI asks the model for JSON matching the schema and validates it before returning. Second, context=[extract_features] will inject the validated JSON into the next task, not the raw model text — so the Writer will not have to guess where the object starts. When a task's downstream consumer is another agent, output_pydantic (or output_json) is almost always the right choice; free-form prose belongs to the last task, the one that produces the file the user reads.
The output_file is the artefact, not a log
Every task can write its result to disk with output_file="path.md". Use it for the outputs you actually want on disk — the drafts, the final documentation, the review report — not for every intermediate step. Two rules keep the folder tidy:
- One folder per crew run, timestamped. A run must not overwrite a previous one; a bad crew that overwrites the good draft of yesterday is worse than a bad crew that produces junk in a new folder.
- Filenames match the schema of the outputs.
sections.jsonfor a JSON array,documentation.mdfor markdown. This is the folder you will hand to the Reviewer in module 6.
Expected outputs that fail well
The Reviewer is the one agent whose expected output should be a verdict, not a rewrite:
review_draft = Task(
description=(
"Review the draft against the source brief. For every claim, verify "
"the source line cited by the Writer actually supports it. For every "
"section, check the house style rules (no marketing verbs, no future tense)."
),
expected_output=(
"A JSON object {verdict: 'approve'|'reject', issues: [{section, line, kind, quote, fix_suggestion}]}. "
"Do not rewrite the draft. Return an empty issues list only if verdict is 'approve'."
),
agent=reviewer,
context=[extract_features, draft_sections],
)
The explicit "do not rewrite the draft" is not decorative. Without it, a helpful Reviewer will silently paste a corrected version and the crew will start iterating on it, breaking the separation module 1 built. This is a small sentence with a large behavioural payoff.
A three-sentence expected_output is fine. A two-paragraph one is instructions in disguise; move them to description and keep the expected output to the shape of the result.
Summary
- Every
Taskneeds adescription, anagentand a testableexpected_output; vagueness here poisons every downstream task. - Declare data flow with
context=[…]rather than string interpolation; failures then propagate cleanly instead of being silently invented around. - Prefer
output_pydanticoroutput_jsonwhen the next task consumes the result — validated JSON removes an entire class of parsing bugs at agent boundaries. - Use
output_filefor artefacts users will read, one timestamped folder per run, filenames that match the schema of the content.
Next module: choosing between a sequential process and a hierarchical one with a Manager agent that decides who runs what.