How do you run a CrewAI Flow and pass initial values into its state?
answer
- instantiate the class, then one call
- an awaitable variant exists too
- inputs land on state, not on parameters
- start methods read them from self.state
- a plot call draws the wiring
basics
~20 sInstantiate 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 sRunning 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
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.
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.
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.
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