Skip to main content

CrewAI Flows

CrewAI Flows is the dedicated orchestration layer that lets you define exactly how tasks and agents execute—whether that’s a simple linear pipeline, a complex conditional tree, or a dynamic loop with human approvals. It moves beyond the predefined sequential and hierarchical processes into a full‑fledged workflow engine that lives inside your Python code. This article is your implementation‑focused handbook: you’ll learn how to create Flows, add tasks, control transitions, manage context, and build robust, production‑ready agent workflows.

What Are Flows in CrewAI

A Flow in CrewAI is an explicit blueprint for execution. It’s a Python class that inherits from crewai.flow.Flow and uses decorators (@start, @listen) to wire tasks together. Instead of just listing tasks and letting the crew run them in order, you define a graph of nodes (tasks) and transitions (listeners) that can branch, loop, or respond to events.

Think of it this way:

  • Task – a piece of work (could be a CrewAI Task object or a plain Python function that calls agents).
  • Agent – the specialized worker that executes a task.
  • Flow – the orchestrator that decides which task runs when, and how data flows between them.

A minimal example:

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

class SimpleResearchFlow(Flow):
@start
def gather_data(self):
# This runs first
return {"topic": "AI trends", "data": "some research"}

@listen(gather_data)
def write_report(self, result):
# This runs after gather_data completes
return f"Report based on: {result['data']}"

flow = SimpleResearchFlow()
flow.kickoff()

Here, @start marks the entry point. @listen(gather_data) means “run this method after gather_data finishes and pass its return value as result”. The Flow engine handles the rest.

Why Flows Matter

Flows give you fine‑grained control over execution in a way that a simple crew cannot.

  • Controlling execution order – Not just A→B→C, but A→ (B or C) depending on data, loops back to A, etc.
  • Managing complex workflows – Multi‑step processes with branching, merging, and conditional paths.
  • Enabling dynamic decision‑making – The output of one step can determine the next step at runtime.
  • Supporting automation pipelines – Flows can be triggered by events and run unattended.
  • Improving reproducibility – The entire execution path is explicit and deterministic (given the same inputs), making debugging far easier.

A marketing crew might need to: research a topic → if competitive keyword exists, run SEO analysis → then generate content; otherwise, skip SEO. With a Flow, that logic is captured directly in the code.

How CrewAI Flows Work

Execution follows a clear pipeline managed by the Flow engine:

Input (kickoff) → Flow Controller → Task Node → Agent Execution (optional) → Transition Evaluation → Next Task Node → … → Final Output

The Flow controller maintains a state object (a Pydantic model you define) that holds all shared data. Each task method receives the current state, updates it, and returns it. Transitions (listeners) fire when the task they’re listening to completes, and they decide the next step.

CrewAI Flows internally use a directed graph model but expose it through a simple, decorator‑based API.

Core Components of Flows

ComponentRole
Flow ControllerThe runtime engine that manages the execution order, state propagation, and transition evaluation.
TasksMethods decorated with @start or @listen. They contain business logic—often invoking a CrewAI Task or agent directly.
AgentsThe AI workers that execute tasks. In Flows, you create agents inside task methods or call them via the Crew API.
StateA Pydantic model that holds all shared data across the Flow. It persists through the entire run.
TransitionsThe @listen decorators that define which task triggers which subsequent task, and under what conditions (via condition parameter).

The state is the central nervous system. You define it as a class:

from crewai.flow.flow import Flow
from pydantic import BaseModel

class ResearchState(BaseModel):
topic: str = ""
raw_data: str = ""
report: str = ""
needs_approval: bool = False

Then, in your Flow, you set model_config or use the state directly in task methods.

Types of Flows in CrewAI

CrewAI Flows support several execution patterns, all using the same decorator system.

1. Sequential Flows

The simplest: each task triggers the next in a chain.

class SeqFlow(Flow):
@start
def step1(self):
...

@listen(step1)
def step2(self, result):
...

@listen(step2)
def step3(self, result):
...

2. Conditional Flows

You can attach conditions to @listen to branch based on state.

@listen(step1, condition="needs_approval")
def approve(self, result):
# runs only if state.needs_approval is True
...

@listen(step1, condition="not needs_approval")
def skip_approval(self, result):
# runs only if False
...

You can also route dynamically inside a listener:

@listen(step1)
def router(self, result):
if self.state.needs_approval:
self.state.next_action = "approve"
else:
self.state.next_action = "skip"
# Then use conditional listeners to pick up the right path

Or simply call a different method manually (though using listeners is recommended for clarity).

3. Event‑Driven Flows

A Flow can be kicked off by an event (e.g., a webhook) and can have listeners that wait for external triggers. While the core Flow runs synchronously after kickoff, you can integrate with event systems by having tasks that poll or using asyncio.

4. Iterative Flows (Loops)

By having a task listen to itself with a condition, you can create refinement loops.

@listen(start_task)
def refine(self, result):
# do work
if self.state.quality < 0.8:
self.state.quality += 0.1
return self.refine() # call itself? Better: use a loop inside the task or a conditional re‑trigger.

A cleaner pattern: use a while loop inside a single task method, or have a listener that conditionally loops back to an earlier task (by listening to that task with a condition). You can use @listen(previous_task) and change the state so that the same listener triggers again if condition met. But since listeners fire exactly once per task completion, true loops require multiple invocations. In practice, you can create an explicit retry loop by calling the task method again inside itself (recursion) or by using a task that self.call_task(...).

CrewAI Flows also provide self.add_task and self.restart methods for advanced dynamic flows.

Flow Execution Lifecycle

Every Flow follows this lifecycle from kickoff() to completion.

  1. Flow initialization – The state object is instantiated (empty or with defaults).
  2. Context setup – Any pre‑kickoff configuration (agents, LLM) is bound.
  3. Task scheduling – The @start task is queued.
  4. Task execution – The task method runs. It can update state, invoke agents, call tools.
  5. Transition evaluation – When the task finishes, its return value (and the state) is passed to all @listen handlers. The Flow engine evaluates conditions and schedules the next task(s).
  6. Output aggregation – The Flow terminates when no more listeners are ready to fire. The final state is returned.

The lifecycle is deterministic: given the same initial state, the same sequence of tasks will execute (assuming the external tools/LLMs are deterministic).

Flow vs Crew vs Tasks

These three abstractions are often confused; here’s how they relate in the Flows paradigm.

ConceptPurposeExample
FlowThe top‑level orchestration that defines the execution logic and state model.class MyFlow(Flow):
CrewA group of agents that execute a set of tasks. You can still use a Crew inside a Flow task for complex multi‑agent sub‑workflows.Crew(agents=[...], tasks=[...])
TaskA unit of work. In Flows, a task is a Python method (possibly calling a CrewAI Task object).def do_research(self):

Flows can replace a simple Crew entirely, or wrap multiple Crews. A Flow’s task method can instantiate a Crew, run it, and return the result—giving you hierarchical composition.

Context Management in Flows

The state object is the shared memory of the Flow. It is automatically persisted between task calls and is available in every method via self.state.

  • Data passing – Task methods read and write to self.state attributes. The return value of a task is passed to the listeners as result, but state changes are already visible globally.
  • State updates – Use self.state.field = value. The state object is a Pydantic model, so validations apply.
  • Persistence – Flows can be configured with a persistence parameter (e.g., flow = MyFlow(persistence=memory_persistence())) to save state between task steps. This allows pausing and resuming long‑running Flows.

Best practice: define a clear state model with distinct fields for each piece of data that needs to be shared.

Flow Control Mechanisms

The true power of Flows comes from the control primitives.

Branching Logic

Use conditions in @listen decorators.

@listen(analyze, condition="self.state.sentiment == 'positive'")
def handle_positive(self, result):
...

@listen(analyze, condition="self.state.sentiment == 'negative'")
def handle_negative(self, result):
...

Looping Control

Achieve loops by having a task that conditionally sets a state flag and then triggers itself or an earlier task via a listener that re‑calls the same method. A simpler approach: inside a task, use a while loop that calls sub‑tasks (like a Crew) multiple times.

Example with conditional re‑trigger:

@listen(refine, condition="self.state.needs_more_refinement")
def refine_again(self, result):
return self.refine() # not recommended to call directly; instead, use a design pattern:
# Option: use a loop in refine itself.

A robust pattern: use a “controller” task that runs in a loop until a condition is met, calling a Crew or agent each iteration.

Conditional Transitions

You can return a string or an enum from a task and use that in listener conditions to pick the next step.

@listen(evaluate)
def router(self, result):
self.state.next_step = result # result could be "escalate" or "close"

@listen(evaluate, condition="self.state.next_step == 'escalate'")
def escalate(self, result):
...

@listen(evaluate, condition="self.state.next_step == 'close'")
def close(self, result):
...

Error‑Based Routing

Wrap task logic in try/except, catch exceptions, update state, and use a listener that triggers a recovery task when self.state.error is set.

@listen(step, condition="self.state.error is not None")
def handle_error(self, result):
...

Error Handling in Flows

Production Flows must survive failures.

  • Task failure handling – Each task method can catch exceptions. If an unhandled exception occurs, the Flow terminates with that error. To avoid that, always wrap critical sections.
  • Retry mechanisms – Implement retry logic inside a task using tenacity. If retries are exhausted, set a state flag and route to a fallback.
  • Fallback flows – Define listeners that act as safety nets. If the primary task fails, a handle_error listener can attempt a simpler alternative.
  • Graceful degradation – If a non‑critical step fails, the Flow can skip it and proceed by setting a state flag that conditions bypass it.
@listen(fetch_data)
def step_with_retry(self, result):
for attempt in range(3):
try:
self.state.data = self.do_fetch()
break
except Exception as e:
if attempt == 2:
self.state.error = str(e)
self.state.data = self.state.data or "fallback data"

Flow Design Patterns

These reusable patterns cover most real‑world scenarios.

PatternDescriptionUse case
PipelineLinear sequence of stepsData processing
Decision TreeBranching based on stateCustomer intent routing
Event PipelineExternal events trigger tasksWebhook‑driven automation
Iterative RefinementLoop until quality threshold metContent review cycles
Human‑in‑the‑LoopPause and wait for human inputApproval workflows

Human‑in‑the‑loop can be implemented by having a task that calls interrupt (or simply sets a state flag and exits), then an external system resumes the Flow by calling flow.resume() with the human input. CrewAI Flows support interrupt natively, similar to LangGraph’s model, using the crewai.flow.flow.interrupt function.

Performance Considerations

  • Parallel execution – Tasks that don’t depend on each other can be configured to run concurrently by using @listen on multiple tasks? Not directly. Flows execute listeners sequentially after a task completes. For parallelism, you can spin up threads or async tasks inside a task method, or use a Crew with async_execution=True inside a Flow step.
  • Bottleneck identification – Monitor duration of each task; use verbose logging to see where time is spent.
  • Reducing unnecessary agent calls – Cache results. Use deterministic checks before invoking LLMs.
  • State optimization – Keep the state model lean; large objects slow serialization and persistence.

Common Beginner Mistakes

  1. Overcomplicated flows – Too many conditional branches make the Flow hard to test. Start simple.
  2. Poor task decomposition – A single task doing too much defeats the purpose of a Flow.
  3. Missing transition rules – Forgetting to add a listener for a particular outcome leaves the Flow stranded.
  4. No error handling – An exception in one task aborts the entire Flow; always plan for failures.
  5. Excessive agent chaining – Each step doesn’t need a separate agent; use tools for simple actions.
  6. Ignoring state persistence – Not setting up persistence means that if the process dies, the Flow is lost.
  7. Using Flows for trivial tasks – A single Crew may be sufficient; don’t over‑engineer.

Best Practices

  • Keep flows simple and readable – Use descriptive method names and keep each task focused.
  • Use explicit transitions – Rely on @listen and conditions rather than hidden side‑effects.
  • Minimize unnecessary steps – Combine trivial operations into one task where it makes sense.
  • Design for failure recovery – Every listener should account for error states.
  • Reuse flow components – Extract common subtasks into separate Flow classes and compose them (using sub‑flows).
  • Maintain structured context – Use a Pydantic state model; document each field.
  • Test each task in isolation – Before integrating, verify the task’s logic and state mutations.

Practical Example: AI Research & Writing Flow

Let’s build a complete Flow that accepts a user topic, performs web research, processes the data, writes a report, and optionally gets human approval.

1. Define the state

from pydantic import BaseModel

class ResearchState(BaseModel):
topic: str = ""
raw_data: list = []
summary: str = ""
draft_report: str = ""
final_report: str = ""
needs_approval: bool = False
approved: bool = False

2. Create the Flow

from crewai.flow.flow import Flow, start, listen
from crewai import Agent, Task, Crew, Process
from crewai_tools import SerperDevTool

class ResearchWritingFlow(Flow[ResearchState]):

@start
def set_topic(self):
self.state.topic = "Impact of AI agents on software engineering" # in real, from input

@listen(set_topic)
def research(self, result):
# Use a CrewAI agent to research
agent = Agent(
role="Researcher",
goal="Find latest data",
tools=[SerperDevTool()],
verbose=True
)
task = Task(
description=f"Research {self.state.topic} and list 5 key trends.",
expected_output="bullet list",
agent=agent
)
crew = Crew(agents=[agent], tasks=[task], process=Process.sequential)
output = crew.kickoff()
self.state.raw_data = output.split('\n') # simplistic

@listen(research)
def process_data(self, result):
# Summarize raw data into a coherent summary
agent = Agent(role="Analyst", goal="Synthesize information", verbose=True)
task = Task(
description="Turn the raw bullet list into a 3-paragraph summary.",
expected_output="3 paragraph summary",
agent=agent,
context=[self.state.raw_data] # pass raw data as string
)
crew = Crew(agents=[agent], tasks=[task])
self.state.summary = crew.kickoff()

# Determine if human approval needed (simulated)
self.state.needs_approval = "controversial" in self.state.summary.lower()

@listen(process_data, condition="needs_approval")
def human_review(self, result):
# Simulate interrupt for human
from crewai.flow.flow import interrupt
decision = interrupt({
"question": "Approve this summary?",
"summary": self.state.summary
})
self.state.approved = decision.get("approved", False)

@listen(process_data, condition="not needs_approval")
def skip_review(self, result):
self.state.approved = True # auto-approve

@listen(human_review, skip_review) # both lead here
def write_report(self, result):
agent = Agent(role="Writer", goal="Create final report", verbose=True)
task = Task(
description=f"Using this summary: {self.state.summary}, write a 500-word markdown report.",
expected_output="markdown report",
agent=agent
)
crew = Crew(agents=[agent], tasks=[task])
self.state.draft_report = crew.kickoff()

@listen(write_report)
def finalize(self, result):
self.state.final_report = self.state.draft_report
# In real, you could trigger an email, save to DB, etc.
print("Flow complete.")
return self.state.final_report

3. Run the Flow

flow = ResearchWritingFlow()
result = flow.kickoff()

Execution path:

  • set_topicresearch (uses search tool) → process_data (summarizes)
  • If needs_approval true, human_review (pauses for human); else skip_review.
  • Then write_report (listens to both) → finalize.
  • The Flow returns the final report.

This demonstrates branching, conditional execution, agent‑crew integration, and human‑in‑the‑loop within a single Flow.

Flows in Real‑World Systems

CrewAI Flows power many production scenarios.

  • Content Automation Pipelines – SEO research → content brief → draft → review → publish.
  • Data Processing Workflows – Extract from APIs → clean → enrich → load to warehouse.
  • Customer Support Systems – Triage intent → retrieve knowledge → compose response → escalate if needed.
  • AI Research Pipelines – Literature search → critical appraisal → synthesis → report.
  • Document Generation Systems – Template selection → data mapping → generation → compliance check.

The explicit control and state management make Flows the go‑to for anything beyond the simplest linear process.

Flows and Other CrewAI Concepts

Flows don’t replace the rest of CrewAI; they orchestrate them.

  • Core Concepts – The agents, tasks, and crews that run inside Flow nodes.
  • Tools & Delegation – Tools are used inside task methods; delegation is achieved by calling different agents in different Flow steps.
  • Production – Flows are production‑ready; with persistence, they can be deployed as services, survive restarts, and be observed.

For integration with external tool ecosystems, see the MCP Guide. For comparisons with other frameworks’ workflow models, refer to the LangGraph comparison and the comprehensive framework comparison.

FAQ

1. What is a Flow in CrewAI?

A Flow is an explicit execution graph that defines the sequence and conditions under which tasks (and their associated agents) run. It uses a Python class with @start and @listen decorators to wire steps together.

2. How is a Flow different from a Crew?

A Crew is a group of agents that execute a set of tasks, typically in a linear or hierarchical fashion. A Flow is the orchestration layer above one or more crews—it can branch, loop, and make dynamic decisions, whereas a Crew’s execution is more rigid.

3. Can Flows run tasks in parallel?

Not directly at the Flow level, but you can achieve parallelism by using async_execution=True on Crew tasks inside a Flow step, or by running multiple crews asynchronously within a single task method.

4. How do conditional flows work?

Using the condition parameter of @listen. The condition is a string expression evaluated against the Flow’s state (e.g., "self.state.needs_approval"). If it evaluates to True, that listener executes.

5. What is Flow context?

The Flow’s state object (a Pydantic model) that holds all shared data across steps. It is the primary way to pass information between tasks.

6. How are errors handled in flows?

You wrap task logic in try/except blocks, set an error flag in the state, and use conditional listeners to route to error‑handling or fallback steps. Unhandled exceptions will abort the Flow.

7. Are Flows deterministic?

Given the same initial state and deterministic task implementations, the execution path will be identical. LLM calls introduce variability; you can reduce it with structured outputs and caching.

8. Can I pause a Flow and resume later?

Yes, CrewAI Flows support persistence. You can pass a persistence parameter when creating the Flow (e.g., persistence=SQLitePersistence()). After pausing (via interrupt()), you can resume with the same Flow instance or a new one that loads the saved state.

9. How do I integrate human‑in‑the‑loop?

Use the interrupt() function inside a task. The Flow will pause and return the interrupt value. An external system can later call flow.resume(input_data) to continue.

10. Can a Flow call other Flows?

Yes, a task method can instantiate and run another Flow, treating it as a sub‑workflow. This enables hierarchical composition.

11. Do I have to use Crews inside a Flow?

No. A Flow task can be any Python logic—direct LLM calls, tool invocations, or plain computation. Crews are useful when you need multi‑agent collaboration for a step.

12. How do I monitor Flow execution?

Set verbose=True on agents and crews. For Flows, log state at each step. Integration with LangSmith or a custom logger is recommended for production.

13. What happens if a listener’s condition is never met?

That listener never fires. This is by design; it allows you to skip entire branches. Ensure you cover all possible outcomes to avoid incomplete workflows.

14. Can I dynamically add tasks at runtime?

Yes, with self.add_task(method, ...). This enables more complex, adaptive workflows. However, it’s an advanced feature; start with static listeners.

15. Are Flows suitable for long‑running processes?

Yes, especially with persistence enabled. A Flow can run for hours, pause for human input, and survive server restarts if the state is stored externally.

16. How do I test a Flow?

Test each task method individually by calling it with a mock state. Then integration‑test the full Flow by kicking it off with different initial states and verifying the final state.

Conclusion

CrewAI Flows give you a powerful, explicit way to orchestrate agent‑based workflows. By moving beyond simple linear execution and embracing a state‑driven, condition‑based model, you can build sophisticated AI pipelines that handle real‑world complexity with clarity.

Key takeaways:

  • Flows are the execution blueprints—they control which tasks run and in what order.
  • State is the shared memory, defined as a Pydantic model, that persists across steps.
  • Listeners and conditions enable branching, looping, and dynamic routing.
  • Error handling, persistence, and human‑in‑the‑loop are built‑in features.
  • Flows can be composed with Crews and tools, giving you the full power of CrewAI with precise control.

Now, continue building your expertise:

For a deeper look at the broader ecosystem, explore the CrewAI framework overview, the LangGraph comparison, or the MCP Guide for tool integration. Start small, then let your Flows orchestrate a symphony of AI agents.