feat: adopt directory-based docs versioning with Edge channel

Switch docs.crewai.com from navigation-only versioning (every version
selector entry rendered the same docs/<lang>/* source files) to
Mintlify's directory-based versioning so each version selector entry
renders its own snapshot. Add an "Edge" channel under docs/edge/<lang>/*
that always reflects main HEAD for unreleased work, eliminating
pre-release leakage onto frozen release labels. External links to
canonical /<lang>/* URLs are preserved via wildcard redirects that
always land on the current default version.

Layout:
- docs/edge/<lang>/*         rolling source (you edit here)
- docs/edge/enterprise-api.*.yaml
- docs/v<X.Y.Z>/<lang>/*     frozen, immutable snapshots
- docs/v<X.Y.Z>/enterprise-api.*.yaml
- docs/images/               shared, append-only
- docs/docs.json             nav + redirects

URLs follow the Mintlify-idiomatic shape: /edge/<lang>/<page> for
Edge, /v<X.Y.Z>/<lang>/<page> for every frozen snapshot. The wildcard
redirects /<lang>/:slug* -> /<default>/<lang>/:slug* keep stale links
working, and every freeze rewrites them (plus all per-section/per-page
redirects) so destinations always resolve to the current default
without depending on a second redirect hop.

Release flow integration (devtools release):
- New module crewai_devtools.docs_versioning.freeze() materialises
  docs/v<X.Y.Z>/ from docs/edge/, rewrites openapi: refs inside the
  snapshot, inserts the version into every language block in
  docs.json, and refreshes all redirect destinations.
- _update_docs_and_create_pr() in cli.py now calls that freeze during
  Phase 2 of devtools release. Edge changelogs are updated first (so
  the snapshot freeze picks them up), then the snapshot is staged
  alongside docs.json, branched as docs/freeze-v<X.Y.Z>, and the PR
  is titled [docs-freeze] docs: snapshot and changelog for v<X.Y.Z>
  — the title prefix the new CI guard reads.
- The PR still gates tag, GitHub release, PyPI publish, and the
  enterprise release as before; no new PRs are added.
- Pre-releases (1.X.YaN, 1.X.YbN, ...) skip the snapshot — they ride
  Edge — and the docs PR title omits the [docs-freeze] prefix.
- docs_check (AI-generated docs scaffolding) writes to
  docs/edge/<lang>/* so newly-generated unreleased docs land in Edge
  and never accidentally touch a frozen snapshot.

Migration scripts (one-shot):
- scripts/docs/freeze_historical_versions.py reconstructs all 16
  historical snapshots (v1.10.0 .. v1.14.7) from git tags via
  git archive | tar, rewriting openapi: MDX refs so each snapshot
  reads its own enterprise-api YAML rather than the live one.
- scripts/docs/prefix_version_paths.py one-shot-migrates docs.json:
  rewrites every page path in 16 versioned blocks to point under
  docs/v<X.Y.Z>/, inserts a new Edge entry per language, tags
  v1.14.7 as Latest (default), prunes pages whose target file
  doesn't exist in the snapshot (e.g. docs/ar/ didn't exist before
  v1.12.0), and writes the wildcard + per-section redirects.
- scripts/docs/freeze_current_edge.py is now a thin CLI wrapper
  around docs_versioning.freeze for manual one-off freezes (e.g.
  retroactively snapshotting a forgotten release).

CI guards (.github/workflows/docs-snapshots.yml):
- Frozen snapshots under docs/v[0-9]*/ are immutable; only PRs whose
  title contains [docs-freeze] (i.e. release-cut PRs generated by
  devtools release or the manual wrapper) may modify them.
- Images under docs/images/ are append-only since snapshots share a
  single image directory. Deleting or renaming an image breaks every
  historical snapshot that still references it.

Restored docs/images/crewai-otel-export.png from PR #3673; it was
deleted in PR #4908 but v1.10.0 / v1.10.1 snapshots still reference
it. Restoring instead of editing the snapshots preserves historical
rendering fidelity and validates the new append-only rule
retroactively.

Tests:
- lib/devtools/tests/test_docs_versioning.py covers the freeze: file
  copy, openapi rewrite, version insertion, default demotion, redirect
  upserts, per-section redirect rewriting, idempotency, and invalid
  inputs.

Verified locally with mintlify broken-links: 0 broken links across
the full site (Edge + 16 frozen versions, 4 locales).

AGENTS.md (repo root) is the contributor guide for the new model;
RELEASING.md is the release-cut runbook; README's Contribution
section links to both.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lucas Gomide
2026-06-17 09:33:56 -03:00
parent 7bb9bc7e1a
commit 93dafe2637
15793 changed files with 3237032 additions and 16873 deletions

View File

@@ -0,0 +1,501 @@
---
title: Conversational Flows
description: Build multi-turn chat apps with handle_turn per turn, message history, intent routing, tracing, and WebSocket bridges.
icon: comments
mode: "wide"
---
## Overview
Conversational apps treat each user line as a **new flow run** with the **same session id**. CrewAI adds helpers for message history, optional intent routing, deferred tracing, UI bridges, and a local `flow.chat()` REPL for conversational flows.
| Concept | Implementation |
|---------|----------------|
| Session id | `handle_turn(..., session_id=...)` → `kickoff(inputs={"id": ...})` → `state.id` |
| User line | `handle_turn(message)` appends to `state.messages` before the graph runs |
| Turn complete | `FlowFinished` for **this run** only; chat continues on the next `handle_turn` |
| Full-session trace | `ConversationConfig(defer_trace_finalization=True)` + `finalize_session_traces()` |
## Turn APIs
Use **`flow.handle_turn(message, session_id=...)`** for every user message from REST, WebSocket, tests, and custom UIs. Use **`flow.chat()`** when you want a local terminal chat loop for a conversational `Flow`.
`Flow.kickoff()` does **not** accept `user_message=` or `session_id=` keyword arguments. For conversational flows, `handle_turn()` stores the pending message and calls `kickoff(inputs={"id": session_id})` internally after resetting per-turn execution state.
| API | Use for |
|-----|---------|
| `handle_turn(message, session_id=...)` | Ergonomic one-turn wrapper for conversational `Flow` |
| `chat()` | Local terminal REPL for conversational `Flow` |
| `kickoff(inputs={...})` | Advanced flow execution without conversational turn handling |
| `ask()` | Blocking prompt **inside** one step (wizard, clarification) |
| `@human_feedback` | Approve/reject **a step output** — not the next chat line |
| `ChatSession.handle_turn(...)` | Transport layer over `handle_turn` (SSE / WebSocket) |
## Quick start
```python
from uuid import uuid4
from crewai import Flow
from crewai.flow import listen
from crewai.experimental.conversational import (
ConversationConfig,
ConversationState,
)
@ConversationConfig(defer_trace_finalization=True)
class SupportFlow(Flow[ConversationState]):
conversational = True
def route_turn(self, context):
message = (self.state.current_user_message or "").lower()
if "order" in message:
return "order"
if "bye" in message or "goodbye" in message:
return "goodbye"
return "help"
@listen("order")
def handle_order(self):
reply = "Your order is on the way."
self.append_assistant_message(reply)
return reply
@listen("help")
def handle_help(self):
reply = "How can I help?"
self.append_assistant_message(reply)
return reply
@listen("goodbye")
def handle_goodbye(self):
reply = "Goodbye!"
self.append_assistant_message(reply)
return reply
session_id = str(uuid4())
flow = SupportFlow()
try:
flow.handle_turn("Where is my order?", session_id=session_id)
flow.handle_turn("What about returns?", session_id=session_id)
finally:
flow.finalize_session_traces() # one trace link for the whole chat
```
## Turn lifecycle
Each `handle_turn` runs this pipeline:
1. **Turn setup** — stores the pending user message, resolves the session id, resets per-turn execution tracking, and calls `kickoff(inputs={"id": session_id})`.
2. **State restore** — if `inputs["id"]` exists and `@persist` is configured, loads the latest snapshot.
3. **`FlowStarted`** — emitted on the first deferred session turn only.
4. **Pending turn hydration** — appends the user message to `state.messages`, sets `current_user_message` / `last_user_message`, and optionally classifies when `intents` / `default_intents` + `intent_llm` are set.
5. **Graph execution** — `conversation_start` → `route_conversation` → the selected `@listen` handler.
6. **End of run** — per-turn `flow_finished` and trace finalization are **skipped** when deferral is enabled; nested `Agent.kickoff()` / crews do not close the parent batch either.
Handlers should call **`append_assistant_message(reply)`** so the next turns `conversation_messages` includes assistant text. The user line is already stored by `handle_turn` — do not append it again in handlers.
## `ConversationConfig` (class-level defaults)
Decorate your conversational `Flow` subclass with `ConversationConfig`.
| Field | Default | Purpose |
|-------|---------|---------|
| `system_prompt` | Framework default | System message used by the built-in `converse_turn`. |
| `llm` | `None` | Conversation LLM used by `converse_turn` and as router fallback. |
| `router` | `None` | `RouterConfig` for LLM-driven routing. |
| `intent_llm` | `None` | LLM for `intents=` / `default_intents` pre-classification. |
| `default_intents` | `None` | Outcome labels for pre-classification. |
| `defer_trace_finalization` | `True` | Keep one trace batch open across `handle_turn()` calls. |
Override pre-classification per turn with `handle_turn(..., intents=..., intent_llm=...)`.
## Lower-level `ChatState` helpers
`ChatState`, `ConversationalConfig`, and `crewai.flow.conversation` helpers are still importable for advanced orchestration, tests, or custom wrappers. They do not add `user_message=` or `session_id=` keyword arguments to `Flow.kickoff()`.
```python
from crewai.flow import ChatState
class MyChatState(ChatState):
# Inherited: id, messages, last_user_message, last_intent, session_ready
research_turn_count: int = 0
custom_flag: bool = False
```
| Field | Role |
|-------|------|
| `id` | Session UUID (same as `inputs["id"]`) |
| `messages` | `list` of `{role, content}` for LLM history |
| `last_user_message` | Latest user line for this turn |
| `last_intent` | Route label after classification (if used) |
| `session_ready` | One-time bootstrap flag (permissions, caches, etc.) |
`ConversationalInputs` is a `TypedDict` for conventional `kickoff(inputs={...})` keys: `id`, `user_message`, `last_intent`.
## `Flow` conversational API
### `handle_turn` parameters
| Parameter | Purpose |
|-----------|---------|
| `message` | This turns text |
| `session_id` | Conversation UUID → `inputs["id"]` / `state.id` |
| `intents` | Outcome labels for pre-kickoff `classify_intent` |
| `intent_llm` | LLM for classification (required with `intents`) |
| `**kickoff_kwargs` | Forwarded to `kickoff()` for options like `input_files`, `from_checkpoint`, and `restore_from_state_id` |
### `kickoff` parameters
`Flow.kickoff()` accepts `inputs`, `input_files`, `from_checkpoint`, and `restore_from_state_id`. Pass `inputs={"id": session_id}` when you need raw flow execution, but use `handle_turn()` when the call represents a chat message.
### Instance attributes
| Attribute | Purpose |
|-----------|---------|
| `conversational` | Set to `True` to enable the conversational graph and `handle_turn()` |
| `defer_trace_finalization` | Instance flag; set automatically from config on `handle_turn()` |
| `suppress_flow_events` | Hides console flow panels; **tracing still records** method/flow events |
| `stream` | Enable streaming; use with `ChatSession.handle_turn(..., stream=True)` |
### Methods and properties
| Name | Description |
|------|-------------|
| `append_assistant_message(content)` | Append a user-visible assistant reply to `state.messages` |
| `append_message(role, content, **extra)` | Lower-level append to `state.messages` |
| `conversation_messages` | Read-only history for LLM calls |
| `classify_intent(text, outcomes, *, llm, context=None)` | Map text to one outcome (same collapse logic as `@human_feedback`) |
| `receive_user_message(text, *, outcomes=None, llm=None)` | Append user message; optionally set `last_intent` |
| `finalize_session_traces()` | Emit deferred `flow_finished` and finalize the session trace batch |
| `_should_defer_trace_finalization()` | Whether this flow defers per-turn trace finalization |
| `input_history` | Audit trail of `ask()` prompts and responses |
### Module helpers (`crewai.flow.conversation`)
Importable for tests or custom orchestration:
| Function | Description |
|----------|-------------|
| `normalize_kickoff_inputs(inputs, user_message=..., session_id=...)` | Merge conversational kwargs into `inputs` |
| `get_conversation_messages(flow)` | Read messages from state or internal buffer |
| `append_message(flow, role, content, **extra)` | Same as instance method |
| `prepare_conversational_turn(flow, user_message=..., intents=..., intent_llm=..., config=...)` | Lower-level turn hydration for custom wrappers |
| `receive_user_message(flow, text, ...)` | Same as instance method |
| `set_state_field(flow, name, value)` | Set a field on dict or Pydantic state |
| `get_conversational_config(flow)` | Read class `conversational_config` |
| `input_history_to_messages(entries)` | Convert `input_history` to LLM message format |
## Intent routing patterns
### A. Pre-classify via `ConversationConfig` (simplest)
Set `default_intents` and `intent_llm`. Each `handle_turn()` runs classification before routing; read `self.state.last_intent` in `route_turn()`.
### B. Classify inside `route_turn` (richer prompts)
Set `default_intents=None` so `handle_turn()` only appends the user message. In `route_turn()`, call `classify_intent` with a custom prompt or descriptions:
```python
def route_turn(self, context):
intent = self.classify_intent(
self._routing_prompt(self.state.current_user_message),
("GREETING", "ORDER", "RESEARCH", "GOODBYE"),
llm="gpt-4o-mini",
)
self.state.last_intent = intent
return intent
```
Use **`@listen("RESEARCH")`** (or similar) for steps that run `Agent.kickoff()` with tools — not bare `LLM.call()` — when you need web research or multi-step tool use.
## When the flow finishes but the user keeps chatting
`FlowFinished` means **this graph run** completed. The conversation continues with another `handle_turn()` and the same `session_id`. `@persist` restores `messages`, flags, and context.
**Persist pattern:** prefer `@persist` on a **single terminal step** (for example `finalize`) rather than on the whole `Flow` class. Class-level persist saves after every method; `load_state` uses the latest row, which may be a mid-run snapshot (for example right after `bootstrap`) and miss handler updates from the same turn.
Do **not** use `@human_feedback` for follow-up chat lines unless a human must approve a specific step output before it is shown.
## Conversational `Flow` (experimental)
<Warning>
**This is an experimental feature.** The conversational `Flow` surface
(`conversational = True`, `handle_turn`, `ConversationConfig`,
`RouterConfig`, `ConversationState`, the built-in graph + helpers) lives
under `crewai.experimental` and may change shape before it graduates.
Pin your CrewAI version if you depend on specific behavior, and watch the
changelog for breaking updates. Open issues / feedback welcome.
</Warning>
Opt into the conversational chat graph by setting `conversational = True` on a `Flow` subclass. The base `Flow` then ships a built-in `@start` / `@router` / `converse_turn` / `end_conversation` graph, manages `state.messages`, can drive a router LLM, and keeps the trace batch open across turns. You write the **custom routes**; the framework owns the rest.
Use this when you want a multi-turn chat with a router and per-route handlers without wiring the lifecycle yourself. Use `Flow[ChatState]` (the lower-level pattern above) when you need full control.
### Quick example
```python
from crewai import Flow
from crewai.flow import listen
from crewai.experimental.conversational import (
ConversationConfig,
ConversationState,
)
@ConversationConfig(defer_trace_finalization=True)
class SupportFlow(Flow[ConversationState]):
conversational = True
def route_turn(self, context: dict) -> str | None:
message = (self.state.current_user_message or "").lower()
if "search" in message or "news" in message:
return "INTERNET_SEARCH"
if "docs" in message or "crewai" in message:
return "CREWAI_DOCS"
return "converse"
@listen("INTERNET_SEARCH")
def handle_internet_search(self) -> str:
"""Fresh web research, current news, real-time lookups."""
reply = "I would run the web research route here."
self.append_assistant_message(reply)
return reply
@listen("CREWAI_DOCS")
def handle_crewai_docs(self) -> str:
"""Look up the CrewAI documentation for framework/API questions."""
reply = "I would look up the CrewAI docs here."
self.append_assistant_message(reply)
return reply
flow = SupportFlow()
try:
flow.handle_turn("What can you do?") # routes to converse
flow.handle_turn("Search the web for AI news.") # routes to INTERNET_SEARCH
flow.handle_turn("Check the CrewAI docs.") # routes to CREWAI_DOCS
finally:
flow.finalize_session_traces()
```
For a local terminal chat, use `chat()`:
```python
def kickoff() -> None:
SupportFlow().chat()
```
`chat()` wraps `handle_turn()` in a REPL, exits on `exit` / `quit`, skips blank lines by default, and calls `finalize_session_traces()` when the session ends.
### `ConversationConfig`
Class decorator that attaches per-class chat defaults.
| Field | Default | Purpose |
|-------|---------|---------|
| `system_prompt` | `slices.conversational_system_prompt` from i18n | System message used by the built-in `converse_turn`. Pass `""` to opt out entirely. |
| `llm` | `None` | Conversation LLM (used by `converse_turn` and as router fallback). |
| `router` | `None` | `RouterConfig` for LLM-driven routing. Without it, the flow always falls through to `converse`. |
| `answer_from_history_prompt` | Framework default | System message for the optional `answer_from_history` route. |
| `answer_from_history_llm` | `None` | Enables the `answer_from_history` short-circuit when set. |
| `intent_llm` | `None` | LLM for legacy `intents=`/`default_intents` pre-classification. |
| `default_intents` | `None` | Outcome labels for legacy pre-classification. |
| `visible_agent_outputs` | `None` | `"all"`, or a list of agent names whose `append_agent_result()` calls should be promoted to public assistant messages. |
| `defer_trace_finalization` | `True` | Keep one trace batch open across `handle_turn()` calls. |
### `RouterConfig` and the auto-built route catalog
```python
from typing import Literal
from pydantic import BaseModel
from crewai import LLM
from crewai.experimental.conversational import RouterConfig
class MyRoute(BaseModel):
intent: Literal["INTERNET_SEARCH", "CREWAI_DOCS", "converse"]
ROUTER_LLM = LLM(model="gpt-4o-mini")
router_config = RouterConfig(
prompt="Optional domain framing (policy, voice, persona).",
response_format=MyRoute, # optional; auto-generated otherwise
llm=ROUTER_LLM, # falls back to ConversationConfig.llm
routes=["INTERNET_SEARCH", "CREWAI_DOCS"], # optional; inferred from listeners
route_descriptions={
"INTERNET_SEARCH": "Override the docstring for this one route.",
},
default_intent="converse", # used when LLM call fails or no LLM available
fallback_intent="converse", # used when LLM returns an invalid route
intent_field="intent",
)
```
The router prompt that gets sent to the LLM is built automatically. For each route the framework picks a description with this precedence:
1. `RouterConfig.route_descriptions[label]` — explicit override.
2. `Flow.builtin_route_descriptions[label]` — framework-canned text for `converse`, `end`, `answer_from_history` (phrased for the router LLM).
3. First non-empty line of the `@listen(label)` handler's docstring.
4. Empty (the route is listed without a description).
So in practice, **adding a new route is `@listen("X")` + a one-line docstring**:
```python
from crewai.flow import listen
@listen("INTERNET_SEARCH")
def handle_internet_search(self) -> str:
"""Fresh web research, current news, real-time lookups."""
...
```
…and the router LLM sees:
```
Routes:
- CREWAI_DOCS: Look up the CrewAI documentation for framework/API questions.
- INTERNET_SEARCH: Fresh web research, current news, real-time lookups.
- converse: Ordinary chat, follow-ups, summaries, clarifications…
- end: User signals the conversation is finished (goodbye, exit, done).
```
`RouterConfig.prompt` is for **domain framing** (assistant persona, business rules, voice). The route catalog is auto-built — don't list routes in `prompt`; they'll drift the moment you add a handler.
### Built-in routes
| Route | Handler | Purpose |
|-------|---------|---------|
| `converse` | `converse_turn` | Default chat handler. Calls `ConversationConfig.llm` with the system prompt + canonical message history. |
| `end` | `end_conversation` | Sets `state.ended = True` and emits a terminator reply. |
| `answer_from_history` | `answer_from_history_turn` | Optional. Routes here when `ConversationConfig.answer_from_history_llm` is set and the message can be answered from existing history. |
You can override any of these by defining a same-named handler in your subclass.
### `handle_turn()` semantics
`flow.handle_turn(message)` runs one turn:
1. Resets per-execution tracking (`_completed_methods`, `_method_outputs`) so the graph re-runs — without this, repeated `kickoff` calls on the same flow instance would short-circuit on turn 2+ because `Flow.kickoff_async` treats `inputs={"id": ...}` as a checkpoint restore.
2. Appends the user message to `state.messages`, sets `current_user_message` / `last_user_message`. `last_intent` is **preserved from the prior turn** so the router LLM can use it as a signal.
3. Runs `conversation_start` → `route_conversation` → the chosen `@listen` handler.
4. The router stores its decision in `state.last_intent` (visible to the next turn's router context).
5. If your handler returned a string and didn't already call `append_assistant_message`, `handle_turn` appends it for you.
Call `handle_turn()` for chat messages. Calling `kickoff(inputs={"id": ...})` directly runs the flow graph without applying the conversational turn wrapper.
### `chat()` for local REPLs
`flow.chat()` is the batteries-included terminal wrapper around `handle_turn()`:
```python
flow = SupportFlow()
flow.chat()
```
It handles the common local loop:
1. Prompts for a user message.
2. Stops on `exit` / `quit`, `EOFError`, or `KeyboardInterrupt`.
3. Calls `handle_turn(message, session_id=...)`.
4. Prints the assistant result.
5. Finalizes deferred session traces in a `finally` block.
Customize the terminal behavior with injectable I/O:
```python
flow.chat(
session_id="demo-session",
prompt="You: ",
assistant_prefix="Assistant: ",
exit_commands=("exit", "quit", "bye"),
)
```
For web apps, background workers, tests, and custom transports, keep using `handle_turn()` directly.
### Custom router behavior
To run side effects (event bus setup, telemetry) on every routing decision, override `route_turn`:
```python
from typing import Any
from crewai import Flow
from crewai.experimental.conversational import ConversationState
class SupportFlow(Flow[ConversationState]):
conversational = True
def route_turn(self, context: dict[str, Any]) -> str | None:
self.event_bus = MyBus(self)
return super().route_turn(context)
```
To bypass the LLM router entirely and pick a route programmatically, return a string from `route_turn`; returning `None` falls back to `_route_with_config(...)`.
### `append_assistant_message` and `append_agent_result`
Inside a `@listen(label)` handler, choose:
- `self.append_assistant_message(text)` — adds a user-visible assistant turn to `state.messages`. The next turn's `converse_turn` sees it.
- `self.append_agent_result(agent_name, result, visibility="private")` — records a structured event in `state.events` and a thread in `state.agent_threads[agent_name]`. Public visibility also calls `append_assistant_message` for you. Use private results for scratch work that shouldn't pollute the canonical history.
`ConversationConfig.visible_agent_outputs` can promote specific agents' private results to public globally (`"all"`, or a list of agent names).
## Tracing across turns
With `defer_trace_finalization=True` (default in `ConversationConfig`):
- **One trace batch** for the whole chat session.
- **`flow_started`** on the first turn only; **`flow_finished`** once in `finalize_session_traces()`.
- **Per-turn** `kickoff` does not print “Trace batch finalized”.
- **Nested work** (`Agent.kickoff()`, crews, Exa tools) appends to the **parent** batch; inner `AgentExecutor` flows do not close the session batch early.
```python
flow.chat(session_id=session_id)
```
`flow.chat()` calls `finalize_session_traces()` for you. When you own the loop
with `handle_turn()`, call `finalize_session_traces()` when
the session ends.
`suppress_flow_events=True` only hides Rich console panels; trace and method events still emit for observability.
### Conversational `Flow` trace lifecycle
The experimental [conversational `Flow`](#conversational-flow-experimental) uses the same tracing lifecycle: `defer_trace_finalization` defaults to `True`, so each `handle_turn()` keeps the session trace open. Always finalize at the end of the session — wrap your REPL/loop in `try/finally` and call `flow.finalize_session_traces()` on exit. Without it, the trace batch stays open and the final conversation may never export.
## Streaming
Set `stream = True` on the `Flow` class. `kickoff(...)` will then emit `assistant_delta` (and related) events through the standard event bus.
## Imports
```python
from crewai.flow import (
ChatState,
ConversationalConfig,
ConversationalInputs,
Flow,
listen,
persist,
router,
start,
)
```
## See also
- [Mastering Flow State Management](/en/guides/flows/mastering-flow-state) — persistence, Pydantic state, `@persist`
- [Build Your First Flow](/en/guides/flows/first-flow) — flow basics
- Demo: `lib/crewai/runner_conversational_flow_simple.py` — minimal REPL with `RESEARCH` + Exa agent

View File

@@ -0,0 +1,552 @@
---
title: Build Your First Flow
description: Learn how to create structured, event-driven workflows with precise control over execution.
icon: diagram-project
mode: "wide"
---
## Taking Control of AI Workflows with Flows
CrewAI Flows represent the next level in AI orchestration - combining the collaborative power of AI agent crews with the precision and flexibility of procedural programming. While crews excel at agent collaboration, flows give you fine-grained control over exactly how and when different components of your AI system interact.
In this guide, we'll walk through creating a powerful CrewAI Flow that generates a comprehensive learning guide on any topic. This tutorial will demonstrate how Flows provide structured, event-driven control over your AI workflows by combining regular code, direct LLM calls, and crew-based processing.
### What Makes Flows Powerful
Flows enable you to:
1. **Combine different AI interaction patterns** - Use crews for complex collaborative tasks, direct LLM calls for simpler operations, and regular code for procedural logic
2. **Build event-driven systems** - Define how components respond to specific events and data changes
3. **Maintain state across components** - Share and transform data between different parts of your application
4. **Integrate with external systems** - Seamlessly connect your AI workflow with databases, APIs, and user interfaces
5. **Create complex execution paths** - Design conditional branches, parallel processing, and dynamic workflows
### What You'll Build and Learn
By the end of this guide, you'll have:
1. **Created a sophisticated content generation system** that combines user input, AI planning, and multi-agent content creation
2. **Orchestrated the flow of information** between different components of your system
3. **Implemented event-driven architecture** where each step responds to the completion of previous steps
4. **Built a foundation for more complex AI applications** that you can expand and customize
This guide creator flow demonstrates fundamental patterns that can be applied to create much more advanced applications, such as:
- Interactive AI assistants that combine multiple specialized subsystems
- Complex data processing pipelines with AI-enhanced transformations
- Autonomous agents that integrate with external services and APIs
- Multi-stage decision-making systems with human-in-the-loop processes
Let's dive in and build your first flow!
## Prerequisites
Before starting, make sure you have:
1. Installed CrewAI following the [installation guide](/en/installation)
2. Set up your LLM API key in your environment, following the [LLM setup
guide](/en/concepts/llms#setting-up-your-llm)
3. Basic understanding of Python
## Step 1: Create a New CrewAI Flow Project
First, let's create a new CrewAI Flow project using the CLI. This command sets up a scaffolded project with all the necessary directories and template files for your flow.
```bash
crewai create flow guide_creator_flow
cd guide_creator_flow
```
This will generate a project with the basic structure needed for your flow.
<Frame caption="CrewAI Framework Overview">
<img src="/images/flows.png" alt="CrewAI Framework Overview" />
</Frame>
## Step 2: Understanding the Project Structure
The generated project has the following structure. The starter embedded crew uses the classic Python/YAML layout, and in Step 4 we will replace the content crew with a JSONC crew.
```
guide_creator_flow/
├── .gitignore
├── pyproject.toml
├── README.md
├── .env
└── src/
└── guide_creator_flow/
├── __init__.py
├── main.py
├── crews/
│ └── poem_crew/
│ ├── config/
│ │ ├── agents.yaml
│ │ └── tasks.yaml
│ └── poem_crew.py
└── tools/
└── custom_tool.py
```
This structure provides a clear separation between different components of your flow:
- The main flow logic in the `src/guide_creator_flow/main.py` file
- Specialized crews in the `src/guide_creator_flow/crews` directory
- Custom tools in the `src/guide_creator_flow/tools` directory
We'll modify this structure to create our guide creator flow, which will orchestrate the process of generating comprehensive learning guides.
## Step 3: Add a Content Writer Crew
Our flow will need a specialized crew to handle the content creation process. Let's use the CrewAI CLI to add a content writer crew:
```bash
crewai flow add-crew content-crew
```
This command automatically creates the necessary directories and template files for your crew. The content writer crew will be responsible for writing and reviewing sections of our guide, working within the overall flow orchestrated by our main application.
## Step 4: Configure the Content Writer Crew
Now, let's configure the content writer crew with JSONC. We'll set up two specialized agents - a writer and a reviewer - that collaborate to create high-quality content for our guide.
1. Create `src/guide_creator_flow/crews/content_crew/agents/content_writer.jsonc`:
```jsonc
{
"role": "Educational Content Writer",
"goal": "Create engaging, informative content that thoroughly explains the assigned topic and provides valuable insights to the reader.",
"backstory": "You are a talented educational writer who explains complex concepts in accessible language and organizes information clearly.",
"llm": "provider/model-id",
"settings": {
"verbose": true
}
}
```
2. Create `src/guide_creator_flow/crews/content_crew/agents/content_reviewer.jsonc`:
```jsonc
{
"role": "Educational Content Reviewer and Editor",
"goal": "Ensure content is accurate, comprehensive, well-structured, and consistent with previously written sections.",
"backstory": "You are a meticulous editor with an eye for detail, clarity, and coherence.",
"llm": "provider/model-id",
"settings": {
"verbose": true
}
}
```
Replace `provider/model-id` with the model you use, for example `openai/gpt-4o`, `gemini/gemini-2.0-flash-001`, or `anthropic/claude-sonnet-4-6`.
3. Create `src/guide_creator_flow/crews/content_crew/crew.jsonc`:
```jsonc
{
"name": "Content Crew",
"agents": ["content_writer", "content_reviewer"],
"tasks": [
{
"name": "write_section_task",
"description": "Write a comprehensive section on the topic: \"{section_title}\".\n\nSection description: {section_description}\nTarget audience: {audience_level} level learners\n\nYour content should begin with a brief introduction, explain key concepts clearly with examples, include practical applications where appropriate, end with a summary, and be approximately 500-800 words.\n\nPreviously written sections:\n{previous_sections}",
"expected_output": "A well-structured, comprehensive section in Markdown format that thoroughly explains the topic and is appropriate for the target audience.",
"agent": "content_writer",
"markdown": true
},
{
"name": "review_section_task",
"description": "Review and improve this section on \"{section_title}\":\n\n{draft_content}\n\nTarget audience: {audience_level} level learners\nPreviously written sections:\n{previous_sections}\n\nFix errors, improve clarity, verify consistency, enhance structure, and add missing key information.",
"expected_output": "An improved, polished version of the section that maintains the original structure but enhances clarity, accuracy, and consistency.",
"agent": "content_reviewer",
"context": ["write_section_task"],
"markdown": true
}
],
"process": "sequential",
"verbose": true
}
```
The `context` field lets the reviewer use the writer's output.
4. Replace `src/guide_creator_flow/crews/content_crew/content_crew.py` with a small loader:
```python
from pathlib import Path
from crewai.project import load_crew
def kickoff_content_crew(inputs: dict):
crew, default_inputs = load_crew(Path(__file__).with_name("crew.jsonc"))
return crew.kickoff(inputs={**default_inputs, **inputs})
```
This loader turns `crew.jsonc` into a `Crew` at runtime. While this crew can function independently, in our flow it will be orchestrated as part of a larger system.
## Step 5: Create the Flow
Now comes the exciting part - creating the flow that will orchestrate the entire guide creation process. This is where we'll combine regular Python code, direct LLM calls, and our content creation crew into a cohesive system.
Our flow will:
1. Get user input for a topic and audience level
2. Make a direct LLM call to create a structured guide outline
3. Process each section sequentially using the content writer crew
4. Combine everything into a final comprehensive document
Let's create our flow in the `main.py` file:
```python
#!/usr/bin/env python
import json
import os
from typing import List, Dict
from pydantic import BaseModel, Field
from crewai import LLM
from crewai.flow.flow import Flow, listen, start
from guide_creator_flow.crews.content_crew.content_crew import kickoff_content_crew
# Define our models for structured data
class Section(BaseModel):
title: str = Field(description="Title of the section")
description: str = Field(description="Brief description of what the section should cover")
class GuideOutline(BaseModel):
title: str = Field(description="Title of the guide")
introduction: str = Field(description="Introduction to the topic")
target_audience: str = Field(description="Description of the target audience")
sections: List[Section] = Field(description="List of sections in the guide")
conclusion: str = Field(description="Conclusion or summary of the guide")
# Define our flow state
class GuideCreatorState(BaseModel):
topic: str = ""
audience_level: str = ""
guide_outline: GuideOutline = None
sections_content: Dict[str, str] = {}
class GuideCreatorFlow(Flow[GuideCreatorState]):
"""Flow for creating a comprehensive guide on any topic"""
@start()
def get_user_input(self):
"""Get input from the user about the guide topic and audience"""
print("\n=== Create Your Comprehensive Guide ===\n")
# Get user input
self.state.topic = input("What topic would you like to create a guide for? ")
# Get audience level with validation
while True:
audience = input("Who is your target audience? (beginner/intermediate/advanced) ").lower()
if audience in ["beginner", "intermediate", "advanced"]:
self.state.audience_level = audience
break
print("Please enter 'beginner', 'intermediate', or 'advanced'")
print(f"\nCreating a guide on {self.state.topic} for {self.state.audience_level} audience...\n")
return self.state
@listen(get_user_input)
def create_guide_outline(self, state):
"""Create a structured outline for the guide using a direct LLM call"""
print("Creating guide outline...")
# Initialize the LLM
llm = LLM(model="openai/gpt-4o-mini", response_format=GuideOutline)
# Create the messages for the outline
messages = [
{"role": "system", "content": "You are a helpful assistant designed to output JSON."},
{"role": "user", "content": f"""
Create a detailed outline for a comprehensive guide on "{state.topic}" for {state.audience_level} level learners.
The outline should include:
1. A compelling title for the guide
2. An introduction to the topic
3. 4-6 main sections that cover the most important aspects of the topic
4. A conclusion or summary
For each section, provide a clear title and a brief description of what it should cover.
"""}
]
# Make the LLM call with JSON response format
response = llm.call(messages=messages)
# Parse the JSON response
outline_dict = json.loads(response)
self.state.guide_outline = GuideOutline(**outline_dict)
# Ensure output directory exists before saving
os.makedirs("output", exist_ok=True)
# Save the outline to a file
with open("output/guide_outline.json", "w") as f:
json.dump(outline_dict, f, indent=2)
print(f"Guide outline created with {len(self.state.guide_outline.sections)} sections")
return self.state.guide_outline
@listen(create_guide_outline)
def write_and_compile_guide(self, outline):
"""Write all sections and compile the guide"""
print("Writing guide sections and compiling...")
completed_sections = []
# Process sections one by one to maintain context flow
for section in outline.sections:
print(f"Processing section: {section.title}")
# Build context from previous sections
previous_sections_text = ""
if completed_sections:
previous_sections_text = "# Previously Written Sections\n\n"
for title in completed_sections:
previous_sections_text += f"## {title}\n\n"
previous_sections_text += self.state.sections_content.get(title, "") + "\n\n"
else:
previous_sections_text = "No previous sections written yet."
# Run the content crew for this section
result = kickoff_content_crew(inputs={
"section_title": section.title,
"section_description": section.description,
"audience_level": self.state.audience_level,
"previous_sections": previous_sections_text,
"draft_content": ""
})
# Store the content
self.state.sections_content[section.title] = result.raw
completed_sections.append(section.title)
print(f"Section completed: {section.title}")
# Compile the final guide
guide_content = f"# {outline.title}\n\n"
guide_content += f"## Introduction\n\n{outline.introduction}\n\n"
# Add each section in order
for section in outline.sections:
section_content = self.state.sections_content.get(section.title, "")
guide_content += f"\n\n{section_content}\n\n"
# Add conclusion
guide_content += f"## Conclusion\n\n{outline.conclusion}\n\n"
# Save the guide
with open("output/complete_guide.md", "w") as f:
f.write(guide_content)
print("\nComplete guide compiled and saved to output/complete_guide.md")
return "Guide creation completed successfully"
def kickoff():
"""Run the guide creator flow"""
GuideCreatorFlow().kickoff()
print("\n=== Flow Complete ===")
print("Your comprehensive guide is ready in the output directory.")
print("Open output/complete_guide.md to view it.")
def plot():
"""Generate a visualization of the flow"""
flow = GuideCreatorFlow()
flow.plot("guide_creator_flow")
print("Flow visualization saved to guide_creator_flow.html")
if __name__ == "__main__":
kickoff()
```
Let's analyze what's happening in this flow:
1. We define Pydantic models for structured data, ensuring type safety and clear data representation
2. We create a state class to maintain data across different steps of the flow
3. We implement three main flow steps:
- Getting user input with the `@start()` decorator
- Creating a guide outline with a direct LLM call
- Processing sections with our content crew
4. We use the `@listen()` decorator to establish event-driven relationships between steps
This is the power of flows - combining different types of processing (user interaction, direct LLM calls, crew-based tasks) into a coherent, event-driven system.
## Step 6: Set Up Your Environment Variables
Create a `.env` file in your project root with your API keys. See the [LLM setup
guide](/en/concepts/llms#setting-up-your-llm) for details on configuring a provider.
```sh .env
OPENAI_API_KEY=your_openai_api_key
# or
GEMINI_API_KEY=your_gemini_api_key
# or
ANTHROPIC_API_KEY=your_anthropic_api_key
```
## Step 7: Install Dependencies
Install the required dependencies:
```bash
crewai install
```
## Step 8: Run Your Flow
Now it's time to see your flow in action! Run it using the CrewAI CLI:
```bash
crewai flow kickoff
```
When you run this command, you'll see your flow spring to life:
1. It will prompt you for a topic and audience level
2. It will create a structured outline for your guide
3. It will process each section, with the content writer and reviewer collaborating on each
4. Finally, it will compile everything into a comprehensive guide
This demonstrates the power of flows to orchestrate complex processes involving multiple components, both AI and non-AI.
## Step 9: Visualize Your Flow
One of the powerful features of flows is the ability to visualize their structure:
```bash
crewai flow plot
```
This will create an HTML file that shows the structure of your flow, including the relationships between different steps and the data that flows between them. This visualization can be invaluable for understanding and debugging complex flows.
## Step 10: Review the Output
Once the flow completes, you'll find two files in the `output` directory:
1. `guide_outline.json`: Contains the structured outline of the guide
2. `complete_guide.md`: The comprehensive guide with all sections
Take a moment to review these files and appreciate what you've built - a system that combines user input, direct AI interactions, and collaborative agent work to produce a complex, high-quality output.
## The Art of the Possible: Beyond Your First Flow
What you've learned in this guide provides a foundation for creating much more sophisticated AI systems. Here are some ways you could extend this basic flow:
### Enhancing User Interaction
You could create more interactive flows with:
- Web interfaces for input and output
- Real-time progress updates
- Interactive feedback and refinement loops
- Multi-stage user interactions
### Adding More Processing Steps
You could expand your flow with additional steps for:
- Research before outline creation
- Image generation for illustrations
- Code snippet generation for technical guides
- Final quality assurance and fact-checking
### Creating More Complex Flows
You could implement more sophisticated flow patterns:
- Conditional branching based on user preferences or content type
- Parallel processing of independent sections
- Iterative refinement loops with feedback
- Integration with external APIs and services
### Applying to Different Domains
The same patterns can be applied to create flows for:
- **Interactive storytelling**: Create personalized stories based on user input
- **Business intelligence**: Process data, generate insights, and create reports
- **Product development**: Facilitate ideation, design, and planning
- **Educational systems**: Create personalized learning experiences
## Key Features Demonstrated
This guide creator flow demonstrates several powerful features of CrewAI:
1. **User interaction**: The flow collects input directly from the user
2. **Direct LLM calls**: Uses the LLM class for efficient, single-purpose AI interactions
3. **Structured data with Pydantic**: Uses Pydantic models to ensure type safety
4. **Sequential processing with context**: Writes sections in order, providing previous sections for context
5. **Multi-agent crews**: Leverages specialized agents (writer and reviewer) for content creation
6. **State management**: Maintains state across different steps of the process
7. **Event-driven architecture**: Uses the `@listen` decorator to respond to events
## Understanding the Flow Structure
Let's break down the key components of flows to help you understand how to build your own:
### 1. Direct LLM Calls
Flows allow you to make direct calls to language models when you need simple, structured responses:
```python
llm = LLM(
model="model-id-here", # gpt-4o, gemini-2.0-flash, anthropic/claude...
response_format=GuideOutline
)
response = llm.call(messages=messages)
```
This is more efficient than using a crew when you need a specific, structured output.
### 2. Event-Driven Architecture
Flows use decorators to establish relationships between components:
```python
@start()
def get_user_input(self):
# First step in the flow
# ...
@listen(get_user_input)
def create_guide_outline(self, state):
# This runs when get_user_input completes
# ...
```
This creates a clear, declarative structure for your application.
### 3. State Management
Flows maintain state across steps, making it easy to share data:
```python
class GuideCreatorState(BaseModel):
topic: str = ""
audience_level: str = ""
guide_outline: GuideOutline = None
sections_content: Dict[str, str] = {}
```
This provides a type-safe way to track and transform data throughout your flow.
### 4. Crew Integration
Flows can seamlessly integrate with crews for complex collaborative tasks:
```python
result = kickoff_content_crew(inputs={
"section_title": section.title,
# ...
})
```
This allows you to use the right tool for each part of your application - direct LLM calls for simple tasks and crews for complex collaboration.
## Next Steps
Now that you've built your first flow, you can:
1. Experiment with more complex flow structures and patterns
2. Try using `@router()` to create conditional branches in your flows
3. Explore the `and_` and `or_` functions for more complex parallel execution
4. Connect your flow to external APIs, databases, or user interfaces
5. Combine multiple specialized crews in a single flow
6. Build multi-turn chat apps with [Conversational Flows](/en/guides/flows/conversational-flows) (`kickoff` per message, `ChatSession`, deferred tracing)
<Check>
Congratulations! You've successfully built your first CrewAI Flow that combines regular code, direct LLM calls, and crew-based processing to create a comprehensive guide. These foundational skills enable you to create increasingly sophisticated AI applications that can tackle complex, multi-stage problems through a combination of procedural control and collaborative intelligence.
</Check>

View File

@@ -0,0 +1,143 @@
---
title: "Migrating from inputs.id to restore_from_state_id"
description: "Move @persist flows off the deprecated inputs.id hydration onto the supported restore_from_state_id field"
icon: "arrow-right-arrow-left"
---
<Warning>
Passing `id` inside `inputs` to hydrate a `@persist` flow is **deprecated** and
scheduled for removal in a future release. The replacement, `restore_from_state_id`,
is available in CrewAI **v1.14.5 and later** — the steps below apply once you
upgrade.
</Warning>
## Overview
The documented way to hydrate a `@persist` flow from a previous execution is to pass
that execution's UUID as `inputs.id`. CrewAI now exposes a dedicated field,
`restore_from_state_id`, that performs the same hydration without overloading the
`inputs` payload — and without coupling the hydration key to the new execution's
identity.
## Migration
If you currently kickoff a `@persist` flow with `inputs={"id": ...}`:
```python
# Deprecated
flow = CounterFlow()
flow.kickoff(inputs={"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv"})
```
Switch to `restore_from_state_id`:
```python
# Supported
flow = CounterFlow()
flow.kickoff(restore_from_state_id="abcd1234-5678-90ef-ghij-klmnopqrstuv")
```
The two modes have different lineage semantics:
- `inputs={"id": <uuid>}` (deprecated) — **resume**: writes land under the supplied
id, extending the same `flow_uuid` history.
- `restore_from_state_id=<uuid>` — **fork**: hydrates state from the snapshot, then
writes under a fresh `state.id`. The source flow's history is preserved.
For most production scenarios — re-running a flow seeded from a previous state — fork
is what you want. See [Mastering Flow State](/en/guides/flows/mastering-flow-state)
for the full mental model.
If you kickoff your flow over the CrewAI AMP REST API, see [AMP](#amp) below for the
equivalent payload migration.
## Why we are deprecating `inputs.id` for `@persist`
`inputs.id` is currently the documented way to resume a `@persist` flow from a
previous execution. The problem is that the same UUID does two jobs at once:
1. **It selects which snapshot `@persist` hydrates from** — load the state saved
under that UUID.
2. **It becomes the new execution's Flow Execution ID** (`state.id` in the SDK;
surfaced as `flow_id` in some contexts) — every `@persist` write from this
kickoff also lands under that same UUID.
This dual role is the root cause of the issues this guide describes. Because the
supplied UUID is also the new execution's id, two kickoffs that pass the same
`inputs.id` are not two distinct executions — they share an id, share a persistence
record, and (on AMP) share a row in the executions list. There is no way to say
"hydrate from this snapshot, but record this run separately" without splitting the
two responsibilities.
`restore_from_state_id` is that split. It tells `@persist` which snapshot to hydrate
from, while leaving the new execution free to receive a fresh `state.id`. The
hydration source and the recorded run are no longer the same UUID — which is what
most production scenarios actually want.
## Removal timeline
`inputs.id` for `@persist` hydration is scheduled for removal in a future release of
CrewAI. There is no immediate hard cut-off — existing flows continue to work — but
once you upgrade to v1.14.5 or later, new code should use `restore_from_state_id`, and
existing flows should migrate at the next convenient opportunity.
## AMP
If you deploy your flow to CrewAI AMP, the migration extends to the kickoff payload
sent to your deployed crew, and the visible symptoms of reusing `inputs.id` show up
on the deployment dashboard. The two subsections below cover both.
### Migrating the kickoff payload
If you currently kickoff a deployed flow by embedding `id` in `inputs`:
```bash
# Deprecated
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CREW_TOKEN" \
-d '{"inputs": {"id": "abcd1234-5678-90ef-ghij-klmnopqrstuv", "topic": "AI Agent Frameworks"}}' \
https://your-crew-url.crewai.com/kickoff
```
Move the UUID to the top-level `restoreFromStateId` field:
```bash
# Supported
curl -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_CREW_TOKEN" \
-d '{
"inputs": {"topic": "AI Agent Frameworks"},
"restoreFromStateId": "abcd1234-5678-90ef-ghij-klmnopqrstuv"
}' \
https://your-crew-url.crewai.com/kickoff
```
`restoreFromStateId` sits next to `inputs` in the kickoff payload, not inside it. The
`inputs` object now only carries values your flow actually consumes.
### What happens when `inputs.id` is reused
When AMP receives a kickoff for a flow whose `inputs.id` matches an existing
execution, it resolves to the existing record rather than creating a new one. From
the deployment dashboard you'll see:
- **Execution status** — the new run's status overwrites the previous run's. A
finished execution can flip back to `running`, or a `completed` run can flip to
`error` if the new kickoff fails — either way the dashboard no longer reflects
the original run.
- **Traces** — OTel traces stack across kickoffs because they share the same
execution id; the previous run's traces are either replaced by, or mixed with,
the new run's. A step-by-step replay no longer corresponds to a single execution.
- **Executions list** — kickoffs that should appear as separate rows collapse into
a single entry, hiding history.
Migrating to `restoreFromStateId` keeps every kickoff as its own execution — with
its own status, traces, and row in the list — while still hydrating state from a
previous run.
<Card title="Need Help?" icon="headset" href="mailto:support@crewai.com">
Contact our support team if you're unsure which mode your flow needs or hit issues
during the migration.
</Card>

View File

@@ -0,0 +1,841 @@
---
title: Mastering Flow State Management
description: A comprehensive guide to managing, persisting, and leveraging state in CrewAI Flows for building robust AI applications.
icon: diagram-project
mode: "wide"
---
## Understanding the Power of State in Flows
State management is the backbone of any sophisticated AI workflow. In CrewAI Flows, the state system allows you to maintain context, share data between steps, and build complex application logic. Mastering state management is essential for creating reliable, maintainable, and powerful AI applications.
This guide will walk you through everything you need to know about managing state in CrewAI Flows, from basic concepts to advanced techniques, with practical code examples along the way.
### Why State Management Matters
Effective state management enables you to:
1. **Maintain context across execution steps** - Pass information seamlessly between different stages of your workflow
2. **Build complex conditional logic** - Make decisions based on accumulated data
3. **Create persistent applications** - Save and restore workflow progress
4. **Handle errors gracefully** - Implement recovery patterns for more robust applications
5. **Scale your applications** - Support complex workflows with proper data organization
6. **Enable conversational applications** - Store and access conversation history for context-aware AI interactions
For multi-turn chat (`kickoff` per user line, `ChatState`, intent routing, deferred tracing, and `ChatSession`), see [Conversational Flows](/en/guides/flows/conversational-flows).
Let's explore how to leverage these capabilities effectively.
## State Management Fundamentals
### The Flow State Lifecycle
In CrewAI Flows, the state follows a predictable lifecycle:
1. **Initialization** - When a flow is created, its state is initialized (either as an empty dictionary or a Pydantic model instance)
2. **Modification** - Flow methods access and modify the state as they execute
3. **Transmission** - State is passed automatically between flow methods
4. **Persistence** (optional) - State can be saved to storage and later retrieved
5. **Completion** - The final state reflects the cumulative changes from all executed methods
Understanding this lifecycle is crucial for designing effective flows.
### Two Approaches to State Management
CrewAI offers two ways to manage state in your flows:
1. **Unstructured State** - Using dictionary-like objects for flexibility
2. **Structured State** - Using Pydantic models for type safety and validation
Let's examine each approach in detail.
## Unstructured State Management
Unstructured state uses a dictionary-like approach, offering flexibility and simplicity for straightforward applications.
### How It Works
With unstructured state:
- You access state via `self.state` which behaves like a dictionary
- You can freely add, modify, or remove keys at any point
- All state is automatically available to all flow methods
### Basic Example
Here's a simple example of unstructured state management:
```python
from crewai.flow.flow import Flow, listen, start
class UnstructuredStateFlow(Flow):
@start()
def initialize_data(self):
print("Initializing flow data")
# Add key-value pairs to state
self.state["user_name"] = "Alex"
self.state["preferences"] = {
"theme": "dark",
"language": "English"
}
self.state["items"] = []
# The flow state automatically gets a unique ID
print(f"Flow ID: {self.state['id']}")
return "Initialized"
@listen(initialize_data)
def process_data(self, previous_result):
print(f"Previous step returned: {previous_result}")
# Access and modify state
user = self.state["user_name"]
print(f"Processing data for {user}")
# Add items to a list in state
self.state["items"].append("item1")
self.state["items"].append("item2")
# Add a new key-value pair
self.state["processed"] = True
return "Processed"
@listen(process_data)
def generate_summary(self, previous_result):
# Access multiple state values
user = self.state["user_name"]
theme = self.state["preferences"]["theme"]
items = self.state["items"]
processed = self.state.get("processed", False)
summary = f"User {user} has {len(items)} items with {theme} theme. "
summary += "Data is processed." if processed else "Data is not processed."
return summary
# Run the flow
flow = UnstructuredStateFlow()
result = flow.kickoff()
print(f"Final result: {result}")
print(f"Final state: {flow.state}")
```
### When to Use Unstructured State
Unstructured state is ideal for:
- Quick prototyping and simple flows
- Dynamically evolving state needs
- Cases where the structure may not be known in advance
- Flows with simple state requirements
While flexible, unstructured state lacks type checking and schema validation, which can lead to errors in complex applications.
## Structured State Management
Structured state uses Pydantic models to define a schema for your flow's state, providing type safety, validation, and better developer experience.
### How It Works
With structured state:
- You define a Pydantic model that represents your state structure
- You pass this model type to your Flow class as a type parameter
- You access state via `self.state`, which behaves like a Pydantic model instance
- All fields are validated according to their defined types
- You get IDE autocompletion and type checking support
### Basic Example
Here's how to implement structured state management:
```python
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel, Field
from typing import List, Dict, Optional
# Define your state model
class UserPreferences(BaseModel):
theme: str = "light"
language: str = "English"
class AppState(BaseModel):
user_name: str = ""
preferences: UserPreferences = UserPreferences()
items: List[str] = []
processed: bool = False
completion_percentage: float = 0.0
# Create a flow with typed state
class StructuredStateFlow(Flow[AppState]):
@start()
def initialize_data(self):
print("Initializing flow data")
# Set state values (type-checked)
self.state.user_name = "Taylor"
self.state.preferences.theme = "dark"
# The ID field is automatically available
print(f"Flow ID: {self.state.id}")
return "Initialized"
@listen(initialize_data)
def process_data(self, previous_result):
print(f"Processing data for {self.state.user_name}")
# Modify state (with type checking)
self.state.items.append("item1")
self.state.items.append("item2")
self.state.processed = True
self.state.completion_percentage = 50.0
return "Processed"
@listen(process_data)
def generate_summary(self, previous_result):
# Access state (with autocompletion)
summary = f"User {self.state.user_name} has {len(self.state.items)} items "
summary += f"with {self.state.preferences.theme} theme. "
summary += "Data is processed." if self.state.processed else "Data is not processed."
summary += f" Completion: {self.state.completion_percentage}%"
return summary
# Run the flow
flow = StructuredStateFlow()
result = flow.kickoff()
print(f"Final result: {result}")
print(f"Final state: {flow.state}")
```
### Benefits of Structured State
Using structured state provides several advantages:
1. **Type Safety** - Catch type errors at development time
2. **Self-Documentation** - The state model clearly documents what data is available
3. **Validation** - Automatic validation of data types and constraints
4. **IDE Support** - Get autocomplete and inline documentation
5. **Default Values** - Easily define fallbacks for missing data
### When to Use Structured State
Structured state is recommended for:
- Complex flows with well-defined data schemas
- Team projects where multiple developers work on the same code
- Applications where data validation is important
- Flows that need to enforce specific data types and constraints
## The Automatic State ID
Both unstructured and structured states automatically receive a unique identifier (UUID) to help track and manage state instances.
### How It Works
- For unstructured state, the ID is accessible as `self.state["id"]`
- For structured state, the ID is accessible as `self.state.id`
- This ID is generated automatically when the flow is created
- The ID remains the same throughout the flow's lifecycle
- The ID can be used for tracking, logging, and retrieving persisted states
This UUID is particularly valuable when implementing persistence or tracking multiple flow executions.
## Dynamic State Updates
Regardless of whether you're using structured or unstructured state, you can update state dynamically throughout your flow's execution.
### Passing Data Between Steps
Flow methods can return values that are then passed as arguments to listening methods:
```python
from crewai.flow.flow import Flow, listen, start
class DataPassingFlow(Flow):
@start()
def generate_data(self):
# This return value will be passed to listening methods
return "Generated data"
@listen(generate_data)
def process_data(self, data_from_previous_step):
print(f"Received: {data_from_previous_step}")
# You can modify the data and pass it along
processed_data = f"{data_from_previous_step} - processed"
# Also update state
self.state["last_processed"] = processed_data
return processed_data
@listen(process_data)
def finalize_data(self, processed_data):
print(f"Received processed data: {processed_data}")
# Access both the passed data and state
last_processed = self.state.get("last_processed", "")
return f"Final: {processed_data} (from state: {last_processed})"
```
This pattern allows you to combine direct data passing with state updates for maximum flexibility.
## Persisting Flow State
One of CrewAI's most powerful features is the ability to persist flow state across executions. This enables workflows that can be paused, resumed, and even recovered after failures.
### The @persist() Decorator
The `@persist()` decorator automates state persistence, saving your flow's state at key points in execution.
#### Class-Level Persistence
When applied at the class level, `@persist()` saves state after every method execution:
```python
from crewai.flow.flow import Flow, listen, start
from crewai.flow.persistence import persist
from pydantic import BaseModel
class CounterState(BaseModel):
value: int = 0
@persist() # Apply to the entire flow class
class PersistentCounterFlow(Flow[CounterState]):
@start()
def increment(self):
self.state.value += 1
print(f"Incremented to {self.state.value}")
return self.state.value
@listen(increment)
def double(self, value):
self.state.value = value * 2
print(f"Doubled to {self.state.value}")
return self.state.value
# First run
flow1 = PersistentCounterFlow()
result1 = flow1.kickoff()
print(f"First run result: {result1}")
# Second run - pass the ID to load the persisted state
flow2 = PersistentCounterFlow()
result2 = flow2.kickoff(inputs={"id": flow1.state.id})
print(f"Second run result: {result2}") # Will be higher due to persisted state
```
#### Method-Level Persistence
For more granular control, you can apply `@persist()` to specific methods:
```python
from crewai.flow.flow import Flow, listen, start
from crewai.flow.persistence import persist
class SelectivePersistFlow(Flow):
@start()
def first_step(self):
self.state["count"] = 1
return "First step"
@persist() # Only persist after this method
@listen(first_step)
def important_step(self, prev_result):
self.state["count"] += 1
self.state["important_data"] = "This will be persisted"
return "Important step completed"
@listen(important_step)
def final_step(self, prev_result):
self.state["count"] += 1
return f"Complete with count {self.state['count']}"
```
#### Forking Persisted State
`@persist` supports two distinct hydration modes on `kickoff` / `kickoff_async`. Use **resume** (`inputs["id"]`) to continue the same lineage; use **fork** (`restore_from_state_id`) to start a new lineage seeded from a snapshot:
| | `state.id` after kickoff | `@persist` writes land under |
|---|---|---|
| `inputs["id"]` (resume) | supplied id | supplied id (extends history) |
| `restore_from_state_id` (fork) | fresh id, or `inputs["id"]` if pinned | new id (source preserved) |
```python
from crewai.flow.flow import Flow, start
from crewai.flow.persistence import persist
from pydantic import BaseModel
class CounterState(BaseModel):
id: str = ""
counter: int = 0
@persist
class CounterFlow(Flow[CounterState]):
@start()
def step(self):
self.state.counter += 1
# Run 1: fresh state, counter 0 -> 1
flow_1 = CounterFlow()
flow_1.kickoff()
# Fork: hydrate from flow_1's latest snapshot, but write under a NEW state.id
flow_2 = CounterFlow()
flow_2.kickoff(restore_from_state_id=flow_1.state.id)
# flow_2 starts with counter=1 (hydrated), then step() bumps it to 2.
# flow_1's flow_uuid history is unchanged.
```
Behavior notes:
- `restore_from_state_id` not found in persistence → the kickoff falls back silently to default behavior (mirrors the existing `inputs["id"]` resume not-found behavior). No exception is raised.
- Combining `restore_from_state_id` with `from_checkpoint` raises a `ValueError` — they target different state systems (`@persist` vs. Checkpointing) and cannot be combined.
- `restore_from_state_id=None` (default) is byte-identical to a kickoff without the parameter.
- Pinning `inputs["id"]` while forking means the new run shares a persistence key with another flow — usually you want only `restore_from_state_id`.
## Advanced State Patterns
### Conditional starts and resumable execution
Flows support conditional `@start()` and resumable execution for HITL/cyclic scenarios:
```python
from crewai.flow.flow import Flow, start, listen, and_, or_
class ResumableFlow(Flow):
@start() # unconditional start
def init(self):
...
# Conditional start: run after "init" or external trigger name
@start("init")
def maybe_begin(self):
...
@listen(and_(init, maybe_begin))
def proceed(self):
...
```
- Conditional `@start()` accepts a method name, a router label, or a callable condition.
- During resume, listeners continue from prior checkpoints; cycle/router branches honor resumption flags.
### State-Based Conditional Logic
You can use state to implement complex conditional logic in your flows:
```python
from crewai.flow.flow import Flow, listen, router, start
from pydantic import BaseModel
class PaymentState(BaseModel):
amount: float = 0.0
is_approved: bool = False
retry_count: int = 0
class PaymentFlow(Flow[PaymentState]):
@start()
def process_payment(self):
# Simulate payment processing
self.state.amount = 100.0
self.state.is_approved = self.state.amount < 1000
return "Payment processed"
@router(process_payment)
def check_approval(self, previous_result):
if self.state.is_approved:
return "approved"
elif self.state.retry_count < 3:
return "retry"
else:
return "rejected"
@listen("approved")
def handle_approval(self):
return f"Payment of ${self.state.amount} approved!"
@listen("retry")
def handle_retry(self):
self.state.retry_count += 1
print(f"Retrying payment (attempt {self.state.retry_count})...")
# Could implement retry logic here
return "Retry initiated"
@listen("rejected")
def handle_rejection(self):
return f"Payment of ${self.state.amount} rejected after {self.state.retry_count} retries."
```
### Handling Complex State Transformations
For complex state transformations, you can create dedicated methods:
```python
from crewai.flow.flow import Flow, listen, start
from pydantic import BaseModel
from typing import List, Dict
class UserData(BaseModel):
name: str
active: bool = True
login_count: int = 0
class ComplexState(BaseModel):
users: Dict[str, UserData] = {}
active_user_count: int = 0
class TransformationFlow(Flow[ComplexState]):
@start()
def initialize(self):
# Add some users
self.add_user("alice", "Alice")
self.add_user("bob", "Bob")
self.add_user("charlie", "Charlie")
return "Initialized"
@listen(initialize)
def process_users(self, _):
# Increment login counts
for user_id in self.state.users:
self.increment_login(user_id)
# Deactivate one user
self.deactivate_user("bob")
# Update active count
self.update_active_count()
return f"Processed {len(self.state.users)} users"
# Helper methods for state transformations
def add_user(self, user_id: str, name: str):
self.state.users[user_id] = UserData(name=name)
self.update_active_count()
def increment_login(self, user_id: str):
if user_id in self.state.users:
self.state.users[user_id].login_count += 1
def deactivate_user(self, user_id: str):
if user_id in self.state.users:
self.state.users[user_id].active = False
self.update_active_count()
def update_active_count(self):
self.state.active_user_count = sum(
1 for user in self.state.users.values() if user.active
)
```
This pattern of creating helper methods keeps your flow methods clean while enabling complex state manipulations.
## State Management with Crews
One of the most powerful patterns in CrewAI is combining flow state management with crew execution.
### Passing State to Crews
You can use flow state to parameterize crews:
```python
from crewai.flow.flow import Flow, listen, start
from crewai import Agent, Crew, Process, Task
from pydantic import BaseModel
class ResearchState(BaseModel):
topic: str = ""
depth: str = "medium"
results: str = ""
class ResearchFlow(Flow[ResearchState]):
@start()
def get_parameters(self):
# In a real app, this might come from user input
self.state.topic = "Artificial Intelligence Ethics"
self.state.depth = "deep"
return "Parameters set"
@listen(get_parameters)
def execute_research(self, _):
# Create agents
researcher = Agent(
role="Research Specialist",
goal=f"Research {self.state.topic} in {self.state.depth} detail",
backstory="You are an expert researcher with a talent for finding accurate information."
)
writer = Agent(
role="Content Writer",
goal="Transform research into clear, engaging content",
backstory="You excel at communicating complex ideas clearly and concisely."
)
# Create tasks
research_task = Task(
description=f"Research {self.state.topic} with {self.state.depth} analysis",
expected_output="Comprehensive research notes in markdown format",
agent=researcher
)
writing_task = Task(
description=f"Create a summary on {self.state.topic} based on the research",
expected_output="Well-written article in markdown format",
agent=writer,
context=[research_task]
)
# Create and run crew
research_crew = Crew(
agents=[researcher, writer],
tasks=[research_task, writing_task],
process=Process.sequential,
verbose=True
)
# Run crew and store result in state
result = research_crew.kickoff()
self.state.results = result.raw
return "Research completed"
@listen(execute_research)
def summarize_results(self, _):
# Access the stored results
result_length = len(self.state.results)
return f"Research on {self.state.topic} completed with {result_length} characters of results."
```
### Handling Crew Outputs in State
When a crew completes, you can process its output and store it in your flow state:
```python
@listen(execute_crew)
def process_crew_results(self, _):
# Parse the raw results (assuming JSON output)
import json
try:
results_dict = json.loads(self.state.raw_results)
self.state.processed_results = {
"title": results_dict.get("title", ""),
"main_points": results_dict.get("main_points", []),
"conclusion": results_dict.get("conclusion", "")
}
return "Results processed successfully"
except json.JSONDecodeError:
self.state.error = "Failed to parse crew results as JSON"
return "Error processing results"
```
## Best Practices for State Management
### 1. Keep State Focused
Design your state to contain only what's necessary:
```python
# Too broad
class BloatedState(BaseModel):
user_data: Dict = {}
system_settings: Dict = {}
temporary_calculations: List = []
debug_info: Dict = {}
# ...many more fields
# Better: Focused state
class FocusedState(BaseModel):
user_id: str
preferences: Dict[str, str]
completion_status: Dict[str, bool]
```
### 2. Use Structured State for Complex Flows
As your flows grow in complexity, structured state becomes increasingly valuable:
```python
# Simple flow can use unstructured state
class SimpleGreetingFlow(Flow):
@start()
def greet(self):
self.state["name"] = "World"
return f"Hello, {self.state['name']}!"
# Complex flow benefits from structured state
class UserRegistrationState(BaseModel):
username: str
email: str
verification_status: bool = False
registration_date: datetime = Field(default_factory=datetime.now)
last_login: Optional[datetime] = None
class RegistrationFlow(Flow[UserRegistrationState]):
# Methods with strongly-typed state access
```
### 3. Document State Transitions
For complex flows, document how state changes throughout the execution:
```python
@start()
def initialize_order(self):
"""
Initialize order state with empty values.
State before: {}
State after: {order_id: str, items: [], status: 'new'}
"""
self.state.order_id = str(uuid.uuid4())
self.state.items = []
self.state.status = "new"
return "Order initialized"
```
### 4. Handle State Errors Gracefully
Implement error handling for state access:
```python
@listen(previous_step)
def process_data(self, _):
try:
# Try to access a value that might not exist
user_preference = self.state.preferences.get("theme", "default")
except (AttributeError, KeyError):
# Handle the error gracefully
self.state.errors = self.state.get("errors", [])
self.state.errors.append("Failed to access preferences")
user_preference = "default"
return f"Used preference: {user_preference}"
```
### 5. Use State for Progress Tracking
Leverage state to track progress in long-running flows:
```python
class ProgressTrackingFlow(Flow):
@start()
def initialize(self):
self.state["total_steps"] = 3
self.state["current_step"] = 0
self.state["progress"] = 0.0
self.update_progress()
return "Initialized"
def update_progress(self):
"""Helper method to calculate and update progress"""
if self.state.get("total_steps", 0) > 0:
self.state["progress"] = (self.state.get("current_step", 0) /
self.state["total_steps"]) * 100
print(f"Progress: {self.state['progress']:.1f}%")
@listen(initialize)
def step_one(self, _):
# Do work...
self.state["current_step"] = 1
self.update_progress()
return "Step 1 complete"
# Additional steps...
```
### 6. Use Immutable Operations When Possible
Especially with structured state, prefer immutable operations for clarity:
```python
# Instead of modifying lists in place:
self.state.items.append(new_item) # Mutable operation
# Consider creating new state:
from pydantic import BaseModel
from typing import List
class ItemState(BaseModel):
items: List[str] = []
class ImmutableFlow(Flow[ItemState]):
@start()
def add_item(self):
# Create new list with the added item
self.state.items = [*self.state.items, "new item"]
return "Item added"
```
## Debugging Flow State
### Logging State Changes
When developing, add logging to track state changes:
```python
import logging
logging.basicConfig(level=logging.INFO)
class LoggingFlow(Flow):
def log_state(self, step_name):
logging.info(f"State after {step_name}: {self.state}")
@start()
def initialize(self):
self.state["counter"] = 0
self.log_state("initialize")
return "Initialized"
@listen(initialize)
def increment(self, _):
self.state["counter"] += 1
self.log_state("increment")
return f"Incremented to {self.state['counter']}"
```
### State Visualization
You can add methods to visualize your state for debugging:
```python
def visualize_state(self):
"""Create a simple visualization of the current state"""
import json
from rich.console import Console
from rich.panel import Panel
console = Console()
if hasattr(self.state, "model_dump"):
# Pydantic v2
state_dict = self.state.model_dump()
elif hasattr(self.state, "dict"):
# Pydantic v1
state_dict = self.state.dict()
else:
# Unstructured state
state_dict = dict(self.state)
# Remove id for cleaner output
if "id" in state_dict:
state_dict.pop("id")
state_json = json.dumps(state_dict, indent=2, default=str)
console.print(Panel(state_json, title="Current Flow State"))
```
## Conclusion
Mastering state management in CrewAI Flows gives you the power to build sophisticated, robust AI applications that maintain context, make complex decisions, and deliver consistent results.
Whether you choose unstructured or structured state, implementing proper state management practices will help you create flows that are maintainable, extensible, and effective at solving real-world problems.
As you develop more complex flows, remember that good state management is about finding the right balance between flexibility and structure, making your code both powerful and easy to understand.
<Check>
You've now mastered the concepts and practices of state management in CrewAI Flows! With this knowledge, you can create robust AI workflows that effectively maintain context, share data between steps, and build sophisticated application logic.
</Check>
## Next Steps
- Experiment with both structured and unstructured state in your flows
- Try implementing state persistence for long-running workflows
- Explore [building your first crew](/en/guides/crews/first-crew) to see how crews and flows can work together
- Check out the [Flow reference documentation](/en/concepts/flows) for more advanced features