DEV Community

Pavel Koryagin
Pavel Koryagin

Posted on

The dual nature of a CrewAI Task. Have you overlooked it too?

Tasks in Crew AI are not what they seem. There’s more to them.

ℹ️ This post is part of the “Crew AI Caveats” series, which I create to fill in the gaps left by official courses and to help you master CrewAI faster and easier.

Let’s take a look at a task definition.

What is a CrewAI task? A YAML with definition and a stereotypical code snippet

We have a separate YAML file with string templates where the values are yet to be interpolated. This YAML file goes into a Python object of class Task.

What do I (and, as it turns out, others too) expect when we see this? The contents of the Task object seem to be a definition of the task, right? With experience in things like n8n or Redux, we expect that the actual data will flow somewhere in memory, according to this definition, and will be returned by kickoff() in the end.

The reality is different.

Key takeaway. A Task object is a task definition and a runtime container at the same time.

Yes, it holds the template strings yet to be interpolated. But it also has the output property of type TaskOutput that gets populated at runtime when the task has reached its final answer.

You can access the output value by reading it on the respective task object.

researcher = Agent(
    config=agents_config['researcher'],
)

research_task = Task(
    config=tasks_config['research_task'],
    agent=researcher,
)

crew = Crew(
    agents=[researcher],
    tasks=[research_task],
    verbose=True,
)

crew.kickoff(inputs=inputs) # Ignoring the return value  

# Reading the output on the task object
print(research_task.output.raw)
# print(research_task.output.json)
# print(research_task.output.to_dict())
# print(research_task.output.pydantic)
Enter fullscreen mode Exit fullscreen mode

Look, in this example, I don’t take the result from the kickoff function. Instead, it is accessed on the task object.

With decorators, it also works, although it looks even more cryptic because you extract the runtime data from a function call that instantiates a Task object... but alright, I complained about the decorators two days ago.

crew_base = SampleCrew()

crew_base.crew().kickoff(inputs=inputs) # Ignoring the return value  

# Reading the output on the task object
print(crew_base.research_task().output.raw)
# print(crew_base.research_task().output.json)
# print(crew_base.research_task().output.to_dict())
# print(crew_base.research_task().output.pydantic)
Enter fullscreen mode Exit fullscreen mode

Knowing this, we can better predict how our crew works:

  • Every task is always a single instance per execution.
    • Maybe in the future, they will make the tasks optional.
    • It is impossible to make them plural without breaking the contract.
  • When you are puzzled about which result you are actually getting from your crew, use your_task.output.raw/json/pydantic for granular control, rather than the result of kickoff().
  • With decorators, you have to create different instances of "CrewBase" for separate runs. There are other bugs with multiple instances, but it mostly works.

And I cannot say this is not reflected in the docs. It is:

Screen from CrewAI docs on task outputs

But to me, this doc makes sense only when I already know how it works. Before that, these parts felt out of context. The courses did not help either. And those Task methods/properties do not exist today, maybe they are from TaskOutput or reserved for the future?

Stay tuned

In the next post: output_pydantic is awesome! But you have to patch the framework.

Top comments (0)