skip to content

Flows

You will learn Flows, CrewAI's event-driven layer that puts deterministic control around non-deterministic crews: @start and @listen steps, @router branching, and typed shared state. Interviewers use it to check you know when to stop letting an LLM choose the control flow.

part ofAI agent & RAG frameworksoverview, primer and where to startread it →
on this pageshow

questions

6

How do you run a CrewAI Flow and pass initial values into its state?

level: juniorimportance: must knowfreq 68%

answer

  1. instantiate the class, then one call
  2. an awaitable variant exists too
  3. inputs land on state, not on parameters
  4. start methods read them from self.state
  5. a plot call draws the wiring

basics

~20 s

Instantiate the Flow subclass and call kickoff(), or kickoff_async() to await it. Pass a dict as the inputs argument and those values are applied to the flow state before the start methods run. kickoff returns the last executed step's output.

solid answer

~40 s

Running a flow is two lines: `flow = MyFlow()` then `flow.kickoff(inputs={"topic": "batteries"})`. The inputs dict is applied to `self.state` before any `@start` method fires, so start methods read `self.state.topic` rather than taking parameters — and with structured Pydantic state the values are validated against the model on the way in. `kickoff_async()` is the awaitable form for running inside an event loop or alongside other work. The return value is the output of the **last** method that executed, which in a branching flow is not necessarily the step you consider final, so it is normal to read the authoritative result off `flow.state` afterwards. For debugging the wiring rather than the run, `flow.plot()` writes an interactive HTML graph of the steps and edges.

go deeper

for a junior

Be able to write the three lines: construct the flow, call kickoff with an inputs dict, and read the result. Know that inputs land on self.state rather than arriving as method parameters.

for a middle

Explain that inputs are applied to state before start methods run and are validated when state is a Pydantic model, that the return value is the last executed step's output, and that plot renders the wiring without running anything.

for a senior

Show the operational habits: authoritative result on state rather than the return value, the run id in every log line, one flow instance per concurrent run, and explicit retry or compensation because an exception simply ends the path.

for a principal

Frame kickoff as the system's entry point — how a request maps to a run id, where concurrency across runs is actually managed, and what your service does with a flow that fails halfway with real side effects already committed.

## Instantiate, then kick off A Flow subclass is a normal class. You create an instance and call `kickoff()` on it: ``` flow = ReportFlow() result = flow.kickoff(inputs={"topic": "solid-state batteries"}) ``` That call fires every `@start` method, dispatches listeners as steps complete, and returns when nothing is left pending. There is no separate build or compile step and no runner object to construct. ## What inputs actually do The `inputs` dict is not passed to your methods as arguments. It is applied to the flow's state *before* the start methods run. So a start method reads what it needs from `self.state`: ``` @start() def outline(self): return f"Outline for {self.state['topic']}" ``` This matters for two reasons. First, with structured Pydantic state the incoming values go through the model, so types are validated and coerced rather than stored blindly — a run started with the wrong shape fails at kickoff rather than three steps in. Second, it keeps the flow testable: because everything a run needs is on state, a test can construct the flow, set state directly, and call a single step in isolation without going through kickoff at all. ## Async `kickoff_async()` is the awaitable variant, for running a flow from within an event loop — a web handler, or alongside other concurrent work. Steps themselves may be declared `async def`, which is how a single step performs concurrent I/O internally. Note the distinction interviewers like: awaiting the flow does not make your synchronous steps concurrent with each other. Fan-out across steps is expressed structurally, with multiple entry points or branches, and rejoined with an explicit join condition — not by switching the kickoff call. ## What comes back `kickoff()` returns the output of the last method that executed. In a straight chain that is the obvious terminal step. In a branching flow, "last executed" depends on which branch a router chose and how branches interleave, so treating the return value as *the* result is fragile. The habit that survives contact with production is to write the authoritative result onto state and read `flow.state` after the call — the flow instance is still yours, and its state is fully populated. The same object also carries the auto-generated run `id`, which is the value to log next to everything the run produced so a later investigation can tie output back to one execution. ## Errors An exception raised inside a step propagates out of `kickoff()`. Downstream listeners never fire, there is no automatic retry, and there is no partial-success return value — but the state the flow accumulated up to that point is still readable on the instance, which is often enough to see how far it got and what the last successful step wrote. If you need retries or compensation, they are code you write inside the step or the caller. ## Seeing the graph `flow.plot()` generates an interactive HTML visualization of the flow's steps and the edges between them. It reads the declarative wiring, so it works without executing anything. It is the fastest way to catch the two classic wiring mistakes — a listener attached to the wrong trigger, and a branch label with no subscriber, which leaves a step floating with no inbound edge. On a flow with more than a handful of steps, plotting it is cheaper than reading the decorators. ## Running the same flow many times Each flow instance owns one state, so running two requests concurrently means two instances — do not reuse one instance across concurrent kickoffs and expect its state to stay coherent. Constructing a flow is cheap; the expensive parts are the crews inside it. For a batch, instantiate per item and, if you want them concurrent, drive the async form from your own gather rather than expecting the flow to parallelize across runs.

  • Why is reading the result off flow.state safer than using the kickoff() return value?
    Because kickoff returns whatever the last executed method returned, and in a branching flow which method that is depends on the router's choice and how branches interleave. Writing the authoritative result to state and reading flow.state after the call is deterministic regardless of the path the run took.
  • Does kickoff_async() make the flow's steps run concurrently?
    No. It makes the whole flow awaitable so you can run it inside an event loop or alongside other work. Concurrency between steps comes from the graph — multiple entry points or parallel branches, rejoined with an explicit join condition — plus async step bodies for concurrent I/O inside a single step.
  • What does flow.plot() give you, and when is it worth running?
    It writes an interactive HTML graph of the flow's steps and edges from the declarative wiring, without executing anything. It is the quickest way to catch a listener attached to the wrong trigger or a branch label nobody subscribes to, both of which otherwise show up as a flow that quietly does less than you expected.
  • What is left behind when a step raises during kickoff?
    The exception propagates out of kickoff, downstream listeners never fire, and nothing is retried. The flow instance is still yours though, so the state accumulated up to the failure is readable and usually tells you how far the run got and what the last successful step wrote. Retry and compensation are code you add.

saying these in an interview costs you the question

  • Thinking kickoff inputs are passed as method arguments
  • Expecting kickoff_async to parallelize the flow's steps
  • Treating the kickoff return value as the definitive result
  • Assuming a failed step is retried automatically
  • Reusing one flow instance across concurrent runs

context

open as a page

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

level: middleimportance: must knowfreq 75%

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.

open as a page

In CrewAI Flows, what does typed Pydantic state give you over dict state?

level: middleimportance: must knowfreq 62%

basics

~20 s

Declaring the flow as Flow of a Pydantic model makes self.state a validated model with attribute access, declared defaults and type checking. Unstructured state is a plain dict you index by key, where a misspelled key silently creates a new one.

open as a page

In CrewAI Flows, when do listeners using or_() and and_() fire?

level: middleimportance: should knowfreq 42%

basics

~20 s

or_() fires the listener each time any of the named steps finishes, so it can run more than once. and_() holds the listener until every named step has finished and then fires it once. or_() is a race, and_() is a join.

open as a page

In CrewAI Flows, how does @router decide which branch runs next?

level: middleimportance: should knowfreq 58%

basics

~20 s

A @router method is triggered like a listener but returns a string label instead of data. Steps decorated with @listen of that exact label run next; branches whose label was not returned simply never execute.

open as a page

What does wrapping each crew in a CrewAI Flow step give you over one big crew?

level: seniorimportance: should knowfreq 55%

basics

~20 s

A flow step is ordinary Python: it calls Crew().kickoff(inputs=...) itself, stores the result on self.state, and a router branches on it. Sequencing, retries and skipping become code you control instead of decisions a manager model makes each run.

open as a page