skip to content

In a CrewAI Flow, what do the @start and @listen decorators do?

level: middleimportance: must knowfreq 75%

answer

  1. one decorator starts, one subscribes
  2. plain Python class, no model needed
  3. fires when kickoff() is called
  4. listener parameter is the upstream return
  5. the rest travels on self.state

basics

~20 s

@start marks methods that run when the flow is kicked off. @listen marks a method that runs after the method it names returns, and it can receive that return value as its argument. Together they wire steps into an event-driven chain.

solid answer

~40 s

A Flow is a plain Python class that subclasses `crewai.flow.flow.Flow`. Decorating a method with `@start()` makes it an entry point: every `@start` method fires when you call `kickoff()`. Decorating a method with `@listen(other_method)` subscribes it to that method's completion event — when the upstream method returns, the listener runs, and if it declares one parameter it receives the upstream return value. Anything that shouldn't be threaded through return values goes on `self.state`, which every step reads and writes. Chaining listeners gives you a deterministic graph of steps expressed in ordinary Python: the sequencing is decided by your decorators, not by a model choosing a next action. `kickoff()` returns the output of the last method that executed, so most flows also park the real result on state.

go deeper

for a junior

Be able to say that @start methods run when the flow is kicked off and @listen methods run after the step they name finishes, and that both are just decorated methods on a Flow subclass.

for a middle

Explain the event model: the listener parameter carries the trigger's return value, everything else travels on self.state, and the graph is static so the framework can plot it. Know that multiple @start methods are allowed.

for a senior

Show judgment about which data goes through return values and which belongs on state, and be ready to explain what happens on an exception — downstream listeners simply never fire, so retries and compensation are your code, not the framework's.

for a principal

Own the boundary question: which parts of a workflow are known enough to hardcode as flow edges and which genuinely need a model to sequence. Argue it in terms of token cost, run-to-run variance and what your team can debug at 3am.

## What a Flow is CrewAI has two layers. A **crew** is non-deterministic: agents with roles and tools decide, within a task, what to do and (in a hierarchical process) who does it next. A **Flow** is the deterministic layer wrapped around that. It is a normal Python class inheriting from `Flow` (imported as `from crewai.flow.flow import Flow, start, listen`), whose methods become steps in an event graph. Nothing about a Flow requires an LLM at all — a step can call a crew, hit a database, run a regex, or do nothing but transform state. The motivation is control. A single large crew hands the model both the *work* and the *order of the work*. When the order is actually known — fetch, enrich, validate, write — paying an LLM to rediscover it every run costs tokens, latency and predictability. Flows let you keep the model where judgment is needed and use plain code everywhere else. ## The @start decorator `@start()` marks an entry point. When you call `MyFlow().kickoff()`, **every** method decorated with `@start()` executes — there can be more than one, and each seeds its own branch of the graph. Note the parentheses: it is a decorator factory, so `@start()` and not `@start`. A start method takes no upstream argument (it has no trigger), but it can read values that `kickoff(inputs={...})` placed on `self.state`. ## The @listen decorator `@listen(trigger)` subscribes a method to the completion of another step. The trigger can be written as the method object inside the class body (`@listen(fetch_data)`) or as its name in a string (`@listen("fetch_data")`). When `fetch_data` returns, the listener is invoked. If the listener declares a single parameter beyond `self`, the upstream return value is passed into it; if it declares none, the return value is simply dropped and the listener is expected to read `self.state`. Listeners chain: a listener's own return value becomes the event that its listeners observe. A method may also be listened to by several listeners, which is how you fan out. And a method can be both a listener and a trigger, which is the normal case in the middle of a pipeline. ## Return values versus self.state Two channels carry data, and choosing badly is the most common design smell. - **Return value** — a point-to-point handoff to the immediate listener. Good for a small result the very next step consumes. - **`self.state`** — a shared object every step can read and write, and the only way for a step to see data produced by something that is not its direct trigger. State is either a plain dict or a Pydantic model, and CrewAI attaches an auto-generated `id` to it either way. A useful rule: return the thing that decides *what happens next*; put on state the thing that later steps *accumulate*. Threading a growing payload through return values across five steps produces methods whose signatures lie about what they need. ## Execution model Execution is event-driven, not a loop you write. `kickoff()` fires the start methods, collects their completion events, dispatches the listeners registered for them, and repeats until no step is pending. There is no scheduler, no polling and no implicit retry: if a step raises, the exception propagates out of `kickoff()` and downstream listeners never fire. Async is supported — a step may be declared `async def`, and `kickoff_async()` awaits the whole flow — but a plain `@listen` is not silently pushed to a background thread. Because the wiring is static, CrewAI can render it: `flow.plot()` writes an interactive HTML graph of the steps and edges, which is the fastest way to catch a listener wired to the wrong trigger. ## Typical shape A production flow usually looks like: one `@start` that normalizes the request onto state, a step that runs the first crew and stores its output, a `@router` that inspects that output, two or three branch steps that run different crews or none at all, and a final step that persists the result. The LLM-heavy parts are three or four bounded crew calls; everything between them is code you can unit-test. ## Failure modes The recurring bugs are: forgetting the parentheses on `@start()`; assuming a listener automatically receives state as its parameter (it receives the *trigger's return value*); writing a listener that mutates state assuming a sibling branch already ran, when nothing orders those two branches; and expecting a listener to fire twice because two different triggers exist — that needs an explicit `or_()` condition rather than two separate decorators.

  • Can a Flow define more than one @start method, and what happens if it does?
    Yes. Every method decorated with `@start()` runs when `kickoff()` is called, so each is an independent entry point seeding its own branch. That is the normal way to launch parallel work; you then rejoin the branches with an explicit join condition rather than assuming any ordering between them.
  • How does a step get data produced by a step that is not its direct trigger?
    Through `self.state`. The parameter passed into a listener is only the return value of the method it listens to. Anything else — accumulated results, the original request, flags set by an earlier branch — has to be written to state by the producer and read from state by the consumer.
  • What does kickoff() actually return when the flow has several branches?
    The output of the final method that executed, which in a branching flow is not always the one you meant. Because of that, most flows treat the return value as convenience only and put the authoritative result on `self.state`, reading it from the flow instance after `kickoff()` returns.

saying these in an interview costs you the question

  • Saying @listen polls or runs on a timer
  • Claiming a Flow may only have one @start method
  • Thinking an LLM chooses which step runs next in a Flow
  • Assuming the listener's parameter is the shared state object
  • Believing steps automatically run in background threads

context