A researcher finds three facts. A writer produces a fluent paragraph containing four. Did adding a second agent improve the work? CrewAI gives us a way to separate responsibilities, but we still need to examine what travels between them.
Before you begin: Understand the agent loop and have a Python environment for optional provider calls.
A role is a prompt, a task is a contract
CrewAI organizes agents, tasks, and a process for running them. An agent's role and goal shape its instructions. They do not confer professional knowledge or independent judgment. A task describes work and its expected output. A crew connects the tasks and agents.
For this exercise, the input is a fictional notice: “W1 starts at 15:00. Capacity is 12. Booking status is not listed.” One task extracts stated facts. A second writes a concise explanation from those facts. A sequential process makes the dependency visible: writing follows extraction.
Before trying two agents, write the desired answer yourself. It should mention the start time and capacity without inventing remaining seats. This reference lets you evaluate behavior instead of admiring how many roles are involved.
What evidence must survive a handoff?
Naming one role “researcher” and another “writer” does not guarantee that evidence reaches the final output. Define what the researcher passes: claims, source locations and unresolved questions. Then define what the writer may do with those records.
Inspect the handoff as data before inspecting the prose. If the sources vanish at that boundary, asking the writer for citations later may encourage reconstruction or invention. The challenge repairs the contract between roles so the final newsletter can preserve the evidence it depends on.
The researcher produces notes with sources; the writer receives only a summary paragraph.
Pass structured claims with their source identifiers and uncertainty.
The writer can retain support for each claim and leave unsupported gaps visible.
Construct a small crew
This is a complete provider integration example, reviewed against CrewAI documentation on September 10, 2026. It has not been run against a paid model here. Install crewai in a compatible virtual environment. Configure the selected provider's credentials and set CREWAI_MODEL to its supported model identifier.
import os
from crewai import Agent, Task, Crew, Process
model = os.environ["CREWAI_MODEL"]
extractor = Agent(
role="Fact extractor",
goal="Preserve only facts stated in the supplied notice.",
backstory="You produce short evidence notes for an editor.",
llm=model,
allow_delegation=False,
max_iter=3,
)
writer = Agent(
role="Notice editor",
goal="Explain the evidence without adding booking claims.",
backstory="You keep unknown information explicit.",
llm=model,
allow_delegation=False,
max_iter=3,
)
extract = Task(
description=(
"Extract facts from this fictional source N1: "
"W1 starts at 15:00. Capacity is 12. "
"Booking status is not listed."
),
expected_output="Facts with source N1; distinguish unknown booking status.",
agent=extractor,
)
write = Task(
description="Explain W1 using only the extracted facts. Preserve unknowns.",
expected_output="Two short sentences with source N1.",
agent=writer,
context=[extract],
)
result = Crew(
agents=[extractor, writer],
tasks=[extract, write],
process=Process.sequential,
verbose=False,
).kickoff()
print(result.raw)
The explicit context relationship tells the writing task which earlier result it needs. The task's expected_output is an instruction, not a schema validator. The iteration bound limits agent work but does not prove task success. None of these fields guarantees that source N1 survives unchanged.
Compare work, not personalities
Inspect the extraction result before reading the final paragraph. If the unknown booking status disappeared during extraction, the writer never received the key qualification. If extraction preserved it but writing dropped it, the second task is the likely failure point.
Record the number of model calls, elapsed time, and accepted answers for the same inputs under one-agent and two-task versions. Use actual observations. Two roles may make debugging easier while still costing more and making the answer worse. The useful architecture is the one that meets your task's evidence and operating requirements.
Repair the handoff
An extractor writes “W1 has 12 seats.” The writer turns that into “12 seats are available.” What information should the intermediate contract carry?
Compare a stronger contract
Carry named facts: capacity: 12, booking_status: unknown, and a source ID. Retain the original evidence as well as the summary. Validate the final answer against these fields. A second agent repeating a first agent's ambiguous wording does not provide independent verification.
For practice, add a contradictory second notice with a different date. Require the extractor to preserve both dates and flag the conflict. The writer should explain the conflict or apply an explicit freshness rule, not silently select its preferred number.
Practice with feedback
Repair a handoff between two agent roles
A researcher agent gathers facts with sources. A writer agent turns them into a newsletter. The newsletter has no sources and one claim nobody can trace.
Treat a task as a contract and stop evidence being lost between roles.
Check your understanding
Your task
Define the handoff structure and the validation between the two roles.
These notes stay on this page. Download them before leaving. Code in this field is not executed.
What to include
- The handoff is structured data with a source id per claim
- Validation runs between the roles and fails loudly
- The writer's output declares which sources it used
- The final check confirms every used id came from the researcher
Compare with a worked answer
Here is one way to answer. Check how it uses the information in the task.
def validate_handoff(items):
problems = []
for i, it in enumerate(items):
for field in ('claim', 'source_id', 'quote'):
if not it.get(field):
problems.append(f'item {i}: missing {field}')
if it.get('quote') and it['quote'] not in load_source(it['source_id']):
problems.append(f"item {i}: quote not found in {it['source_id']}")
if problems:
raise HandoffError(problems) # the writer never runs on bad evidence
return items
def validate_output(writer_out, items):
allowed = {it['source_id'] for it in items}
used = set(writer_out['used_source_ids'])
unknown = used - allowed
if unknown:
raise OutputError(f'cited sources not in the handoff: {sorted(unknown)}')
if not used:
raise OutputError('newsletter cites nothing; every draft must be traceable')
return writer_out
# Running this on the failing newsletter: validate_handoff passed with 6 items,
# validate_output raised because used_source_ids was empty. The writer prompt
# asked for 'an engaging newsletter' and never mentioned sources, so the role
# description was doing no work at all. Adding the field to the contract, and
# the check after it, fixed the untraceable claim in one change - not by
# rewriting the writer's persona, which is where I spent the first hour.When you are signed in, opening the challenge carries your edited working notes into its draft in this browser. The challenge has its own completion record. Practising here does not award points or mark it complete.
Next, compare these frameworks on one shared task so that changes in the workload do not masquerade as improvements in the framework.
Sources
CrewAI agents documents direct agent configuration. Crews explains execution processes and outputs. Tasks describes task context and result contracts.