Flows: start, listen and state

CrewAI · Practical · 14 min

Flows give you deterministic, testable control over multi-step AI apps, with crews only where needed.

A Flow is a Python class whose methods are steps. Decorators (the @name lines above a method) say when each step runs:

  • @start() marks an entry point. It runs when the flow starts.
  • @listen(step) runs a method after step finishes. It receives that step's return value as an argument.
class Pipeline(Flow):
    @start()
    def fetch(self):
        return "raw text"

    @listen(fetch)
    def clean(self, text):
        return text.strip()

Pipeline().kickoff() runs the flow and returns the output of the last step that finished.

State is data shared by all steps of one run. Two styles:

  • Unstructured: self.state behaves like a dictionary: self.state["count"] = 1.
  • Structured: subclass Flow[MyState], where MyState is a Pydantic model (a class with typed, validated fields). Then use self.state.count. This catches typos and documents the data. Prefer it beyond toys.

Every run's state has a unique id, used later for saving and resuming.

You can pass values in with kickoff(inputs={...}); matching state fields are filled before the first step.

Steps can contain anything: plain code, a direct LLM call, agent.kickoff(...) for a single agent, or a whole crew. That is the point of flows: AI where you need it, normal code everywhere else.

flow.plot("my_flow") writes an HTML diagram of the steps, useful for reviews.

Example

from pydantic import BaseModel
from crewai.flow import Flow, listen, start

class TicketState(BaseModel):
    text: str = ""
    words: int = 0
    summary: str = ""

class TicketFlow(Flow[TicketState]):
    @start()
    def receive(self):
        self.state.words = len(self.state.text.split())
        return self.state.text

    @listen(receive)
    def summarize(self, text):
        # Replace with an agent or crew call in a real app
        self.state.summary = text[:40] + ("..." if len(text) > 40 else "")
        return self.state.summary

flow = TicketFlow()
out = flow.kickoff(inputs={"text": "My invoice for March shows a double charge, please fix."})
print(out)                # My invoice for March shows a double char...
print(flow.state.words)   # 10

Remember

  • @start() steps run first; @listen(step) runs after step, receiving its result
  • Structured state: Flow[PydanticModel] and self.state.field
  • Steps mix plain code, LLM calls, single agents and crews

Watch out for

  • Using untyped dictionary state in a large flow, where a typo in a key silently creates a new value.

Check yourself

  1. What does a method decorated with @listen(fetch) receive as its argument?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.crewai.com/en/concepts/flows