Downstream code needs reliable fields, not paragraphs you have to parse with guesswork.
By default a task returns free text. When code will use the result, ask for structured output instead.
Describe the shape with a Pydantic model: a class whose typed fields define the data, which Pydantic validates. Then set it on the task:
task = Task(..., output_pydantic=Lead)After the run:
task_output.pydantic(orresult.pydanticon the crew, for the last task) is aLeadinstance with real typed fields.output_json=Leadis an alternative that gives a plain dictionary injson_dict.rawstill holds the text.
Set only one of output_pydantic and output_json per task.
Field descriptions (Field(description=...)) are part of what the model sees, so write them as instructions. Use Literal["a", "b"] for fixed categories and keep schemas small.
One agent, no crew. For quick jobs you can call an agent directly with agent.kickoff(messages). Pass response_format=Lead to get structured output. The result's .pydantic holds the object and .raw the text. This skips tasks and crews entirely and is handy inside flows.
On the model itself. LLM(model=..., response_format=Lead) asks the provider for that format on every call. Support differs by provider, so test it.
If validation fails, CrewAI tries to convert the output into the schema. Repeated failures usually mean the schema or the task description is unclear.
Example
from typing import Literal
from pydantic import BaseModel, Field
from crewai import Agent, Crew, Task
class Lead(BaseModel):
company: str
size: Literal["small", "medium", "large"]
interest: int = Field(ge=1, le=5, description="1 = cold, 5 = ready to buy")
qualifier = Agent(role="Sales qualifier", goal="Score inbound leads",
backstory="You read emails and judge buying intent.")
task = Task(description="Qualify this email: {email}",
expected_output="The lead's company, size and interest.",
agent=qualifier, output_pydantic=Lead)
result = Crew(agents=[qualifier], tasks=[task]).kickoff(
inputs={"email": "Hi, we're a 40-person agency and need a quote this week."})
lead = result.pydantic
print(lead.size, lead.interest) # medium 5 (model-dependent)
quick = qualifier.kickoff("Qualify: 'Just browsing, 2 people.'", response_format=Lead)
print(quick.pydantic) # company='...' size='small' interest=1Remember
output_pydantic=Modelon a task gives.pydanticobjectsagent.kickoff(..., response_format=Model)for one-off structured calls- Field descriptions and Literal types guide the model
Watch out for
- Setting both output_pydantic and output_json on the same task; choose one.