LangGraph Section Flow Middleware
Section-based flow control for LangGraph React agents. Divide a conversational agent into discrete, self-contained phases — each with its own prompt, tools, and transition rules — without writing a custom graph.
Why section flow?
A typical React agent sees all tools and the full system prompt on every call. As workflows grow (qualification → recommendation → booking → payment), this creates two problems:
- Context pollution — the agent is distracted by tools and instructions that are irrelevant to the current step.
- Unclear guardrails — it's hard to restrict what the agent can do at each stage without building a bespoke multi-node graph.
SectionFlowMiddleware solves both by layering a lightweight state machine
on top of create_react_agent. You describe sections in plain Python; the
middleware handles everything else.
Features
| Feature | Description |
|---|---|
| Section-scoped tools | Only the tools listed for the active section are visible to the model |
| Section-scoped prompts | Phase instructions are injected as a prepended system message (prompt-cache friendly) |
| Auto-transitions | Conditions evaluated before every model call advance the flow automatically |
| Agent-initiated transitions | The built-in change_section tool lets the model move itself through the workflow |
| Per-section LLM | Swap to a different model for a specific phase (e.g. a cheaper model for data-gathering) |
| Strict validation | Optionally require specific state fields before entering a section |
| Fallback sections | Gracefully recover when persisted state references a section that no longer exists |
| Global tool overrides | Certain tools can span all sections and override section-level counterparts |
Installation
Install the latest release from PyPI:
pip install langgraph-state-machine
You'll also need a model provider package for whichever LLM you use, e.g.:
pip install langchain-openai # OpenAI
pip install langchain-anthropic # Anthropic
Requirements: Python ≥ 3.10, langchain ≥ 1.0.0, langgraph ≥ 0.2.0
To hack on the library itself:
git clone https://github.com/mahmoud661/langgraph-state-machine
cd langgraph-state-machine
pip install -e ".[dev]"
pytest
How to use it
Using the middleware is four steps: define your tools, group them into
sections, wrap the sections in a SectionFlowMiddleware, and pass that
middleware to your agent. The package installs as section_flow:
import os
from langchain.agents import create_react_agent
from langchain_core.tools import tool
from langchain_openai import ChatOpenAI
from section_flow import SectionFlowMiddleware, SectionConfig
# --- 1. Define your tools ------------------------------------------------
@tool
def save_user_info(name: str, budget: int, category: str) -> str:
"""Save the user's name, budget, and product category."""
return f"Saved: name={name}, budget={budget}, category={category}"
@tool
def search_catalog(query: str, max_price: int) -> str:
"""Search the product catalog and return matching items."""
return f"Top results for '{query}' under ${max_price}: ..."
@tool
def process_payment(item_name: str, amount: float) -> str:
"""Process payment for the selected item."""
return f"Payment of ${amount:.2f} for '{item_name}' processed."
# --- 2. Describe your workflow as sections -------------------------------
sections = {
"gather": SectionConfig(
name="gather",
prompt=(
"Collect the user's name, budget, and product category. "
"Once you have all three, call save_user_info and transition to 'recommend'."
),
tools=[save_user_info],
allowed_transitions=["recommend"],
),
"recommend": SectionConfig(
name="recommend",
prompt="Present exactly three options, then move to 'checkout' when the user picks one.",
tools=[search_catalog],
allowed_transitions=["checkout"],
),
"checkout": SectionConfig(
name="checkout",
prompt="Confirm the item and total price, then process payment.",
tools=[process_payment],
allowed_transitions=[], # terminal section
),
}
# --- 3. Create the middleware ---------------------------------------------
middleware = SectionFlowMiddleware(
sections=sections,
initial_section="gather",
fallback_section="gather",
)
# --- 4. Attach it to your agent -------------------------------------------
agent = create_react_agent(
model=ChatOpenAI(model="gpt-4o", api_key=os.environ["OPENAI_API_KEY"]),
system_prompt="You are a friendly shopping assistant.",
middleware=[middleware],
)
result = agent.invoke(
{"messages": [{"role": "user", "content": "Hi, I need a new laptop."}]}
)
print(result["current_section"]) # section the agent ended in
print(result["messages"][-1].content)
The agent starts in gather and only sees save_user_info plus the built-in
change_section tool. When the model decides it has what it needs, it calls
change_section(target_section="recommend") — from then on it sees the
recommend prompt and tools instead. You never write graph nodes or edges;
the middleware rewrites the prompt and tool list before every model call
based on current_section in the agent state.
Runnable end-to-end scripts live in examples/.
Core concepts
SectionConfig
Each section is a pydantic.BaseModel:
SectionConfig(
name="gather", # unique identifier
prompt="...", # injected system message fragment
tools=[my_tool], # tool objects available in this section
allowed_transitions=["recommend"], # sections this one may transition to
required_state_fields={"budget": int},# fields that must exist in section_data before entering
auto_transition_conditions=lambda state: (
"recommend" if state.get("section_data", {}).get("budget") else None
),
strict_validation=True, # enforce allowed_transitions and required_state_fields
on_enter=lambda state: None, # lifecycle hook (called on entry)
on_exit=lambda state: None, # lifecycle hook (called on exit)
llm=ChatOpenAI(model="gpt-4o-mini"), # optional per-section model override
allowed_subagents=["research_agent"], # limit task-tool subagents for this section
)
SectionFlowState
The middleware extends your agent state with three fields:
class MyState(SectionFlowState): # merge with your existing state
messages: Annotated[list, add_messages]
# Fields added by the middleware:
# current_section – name of the active section
# section_data – shared dict for cross-section data (e.g. {"budget": 1000})
# visited_sections – ordered list of activated sections
Transitions
There are three ways to advance the flow:
| Method | When to use |
|---|---|
| Auto-transition | Data-driven: move when section_data satisfies a condition |
change_section tool |
Agent-driven: model explicitly calls the tool |
before_model fallback |
Safety net: fallback section for missing/removed sections |
Auto-transition (callable)
SectionConfig(
name="gather",
auto_transition_conditions=lambda state: (
"recommend"
if state.get("section_data", {}).get("budget")
else None
),
...
)
Auto-transition (priority list)
from section_flow import TransitionCondition
SectionConfig(
name="gather",
auto_transition_conditions=[
TransitionCondition(
target="vip_recommend",
condition=lambda s: s.get("section_data", {}).get("budget", 0) > 5000,
priority=10,
),
TransitionCondition(
target="recommend",
condition=lambda s: bool(s.get("section_data", {}).get("budget")),
priority=0,
),
],
...
)
Per-section LLM
from langchain_openai import ChatOpenAI
SectionConfig(
name="checkout",
prompt="Process payment carefully.",
tools=[process_payment],
llm=ChatOpenAI(model="gpt-4o"), # override the graph's default model here
)
Advanced usage
Pre-built SectionManager
Reuse the same manager across multiple agents:
from section_flow import SectionManager, SectionFlowMiddleware
manager = SectionManager(
sections=sections,
initial_section="gather",
fallback_section="gather",
)
agent1 = create_react_agent(..., middleware=[SectionFlowMiddleware(section_manager=manager)])
agent2 = create_react_agent(..., middleware=[SectionFlowMiddleware(section_manager=manager)])
Global tools
Tools that should always be available regardless of section:
SectionFlowMiddleware(
sections=sections,
initial_section="gather",
global_tools=[escalate_to_human], # overrides section tools with same name
)
Disable the transition tool
If you prefer purely automatic or state-driven transitions:
SectionFlowMiddleware(
sections=sections,
initial_section="gather",
include_transition_tool=False,
)
Runtime cache invalidation
If you modify sections at runtime (hot-reload), clear the tool cache:
middleware.clear_tool_cache()
API reference
SectionFlowMiddleware
| Parameter | Type | Default | Description |
|---|---|---|---|
sections |
dict[str, SectionConfig] |
— | Section registry |
initial_section |
str |
— | Starting section |
strict_validation |
bool |
True |
Enforce transition and field rules |
include_transition_tool |
bool |
True |
Register change_section tool |
section_manager |
SectionManager |
None |
Pre-built manager (overrides above) |
fallback_section |
str |
initial_section |
Fallback for removed sections |
global_tools |
list |
[] |
Tools available in every section |
subagent_graphs |
dict|list |
{} |
Subagent registry for task-tool filtering |
all_middleware |
list |
[] |
Other middleware for string-name tool resolution |
SectionConfig
| Field | Type | Default | Description |
|---|---|---|---|
name |
str |
required | Unique section identifier |
prompt |
str |
required | System message fragment injected when active |
tools |
list |
[] |
Tool objects (or "task") available here |
allowed_transitions |
list[str] |
[] |
Reachable sections (empty = all allowed) |
required_state_fields |
dict[str, type] |
{} |
Fields required in section_data to enter |
auto_transition_conditions |
callable or list | None |
Conditions evaluated before each model call |
strict_validation |
bool |
True |
Enforce rules for this section |
on_enter |
callable | None |
Called when section activates |
on_exit |
callable | None |
Called when section deactivates |
llm |
any | None |
Model override for this section |
allowed_subagents |
list[str] |
None |
Subagents available via task tool |
Examples
See the examples/ directory:
| File | What it demonstrates |
|---|---|
01_basic_sections.py |
Three-section shopping assistant with agent-initiated transitions |
02_auto_transitions.py |
Data-driven auto-transitions using TransitionCondition |
03_per_section_llm.py |
Swapping models per section to balance quality and cost |
How it works
User message
│
▼
before_model() ← initialise state, resolve fallbacks, fire auto-transitions
│
▼
wrap_model_call() ← prepend section prompt, filter tools, swap model if needed
│
▼
LLM call
│
▼
[agent calls change_section tool] ← updates current_section in state
│
▼
next before_model() ...
Contributing
See CONTRIBUTING.md.
License
Apache 2.0 — see LICENSE.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file langgraph_state_machine-0.1.1.tar.gz.
File metadata
- Download URL: langgraph_state_machine-0.1.1.tar.gz
- Upload date:
- Size: 28.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
daeda239af13171b41069ebd56c01980b46b6b8aa6779dea0afe926a4d5e91a8
|
|
| MD5 |
335f884491b28612711f0b86a5323ac6
|
|
| BLAKE2b-256 |
047a4ad3fd59566a8fde7b5e3b69bbd7b6bd3440b25028ab5bbaf2ea316621fa
|
Provenance
The following attestation bundles were made for langgraph_state_machine-0.1.1.tar.gz:
Publisher:
release.yml on mahmoud661/langgraph-state-machine
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
langgraph_state_machine-0.1.1.tar.gz -
Subject digest:
daeda239af13171b41069ebd56c01980b46b6b8aa6779dea0afe926a4d5e91a8 - Sigstore transparency entry: 2339879523
- Sigstore integration time:
-
Permalink:
mahmoud661/langgraph-state-machine@fa6931d6145bfb3baaa7ead29e0cffcc9da78209 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/mahmoud661
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@fa6931d6145bfb3baaa7ead29e0cffcc9da78209 -
Trigger Event:
push
-
Statement type:
File details
Details for the file langgraph_state_machine-0.1.1-py3-none-any.whl.
File metadata
- Download URL: langgraph_state_machine-0.1.1-py3-none-any.whl
- Upload date:
- Size: 22.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
2d8b42ecd9ba3a3aedd595fa0320d67599465a323e0bf1c8bfae56cae6cb6622
|
|
| MD5 |
acc8588115b5feec5eb456db4cab3338
|
|
| BLAKE2b-256 |
a44721295533cb2dd00068fb7904b2b53cb905b39ecc2f9b738dc431507488a5
|
Provenance
The following attestation bundles were made for langgraph_state_machine-0.1.1-py3-none-any.whl:
Publisher:
release.yml on mahmoud661/langgraph-state-machine
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
langgraph_state_machine-0.1.1-py3-none-any.whl -
Subject digest:
2d8b42ecd9ba3a3aedd595fa0320d67599465a323e0bf1c8bfae56cae6cb6622 - Sigstore transparency entry: 2339879567
- Sigstore integration time:
-
Permalink:
mahmoud661/langgraph-state-machine@fa6931d6145bfb3baaa7ead29e0cffcc9da78209 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/mahmoud661
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@fa6931d6145bfb3baaa7ead29e0cffcc9da78209 -
Trigger Event:
push
-
Statement type: